How Pyrefly Handles Attribute Access and Property Types
Pyrefly implements Python’s attribute lookup rules through a three-phase pipeline that combines static MRO resolution, dynamic fallback via __getattr__, and strict descriptor protocol semantics to accurately type-check property accesses.
The facebook/pyrefly type checker models Python’s complex attribute resolution mechanism exactly as the language specification defines, enabling precise static analysis of both static class attributes and dynamic property descriptors. Understanding how Pyrefly resolves attribute access is essential for interpreting type errors related to missing attributes, read-only properties, or incompatible setter types. This article examines the core resolution logic, descriptor handling, and property type inference implemented in the Rust source code.
The Three Phases of Attribute Resolution
Pyrefly’s attribute resolution follows CPython’s canonical lookup order, implemented primarily in the lsp_attributes.rs module. The system evaluates each attribute access through distinct static and dynamic phases before applying descriptor semantics.
Static Attribute Resolution
When an attribute name is known at analysis time, Pyrefly initiates static resolution by walking the class’s Method Resolution Order (MRO). The entry point for this operation is the resolve_attribute_definition function defined in pyrefly/lib/state/lsp_attributes.rs at approximately line 1485.
This function receives the expression handle, attribute name, and definition context, then traverses the class hierarchy to locate the attribute definition. If found, it returns the definition object; if the lookup fails, it returns None, signaling either a static error or the need for dynamic fallback. Static resolution covers explicitly annotated class attributes, methods, and imported symbols.
Dynamic Fallback via __getattr__ and __getattribute__
When static resolution fails to locate an attribute, Pyrefly checks for dynamic attribute handlers. If the class defines __getattr__ or __getattribute__, the resolver treats the attribute as implicitly defined, deriving its type from the return annotation of these dunder methods.
This logic is implemented immediately following the static lookup in lsp_attributes.rs. The test suite in pyrefly/lib/test/attributes.rs validates this behavior through cases like test_object_getattr, demonstrating that accesses on classes with custom __getattr__ implementations receive the method’s declared return type rather than triggering "attribute missing" errors.
Descriptor Protocol and Property Types
Once an attribute is located—either statically or dynamically—Pyrefly applies descriptor protocol semantics. The core helper resolve_descriptor (in the same file) examines whether the attribute value implements __get__, __set__, or __delete__.
For property objects specifically, Pyrefly extracts the type signatures from the getter, setter, and deleter:
- The getter’s return type becomes the attribute’s read type
- The setter’s argument type becomes the write type
- Missing getters mark the property as write-only; missing setters mark it read-only
Data vs. Non-Data Descriptors
Pyrefly strictly enforces Python’s precedence rules for data descriptors versus non-data descriptors. Data descriptors (defining both __get__ and either __set__ or __delete__) take precedence over instance dictionary entries, while non-data descriptors are shadowed by instance attributes.
This precedence logic lives in resolve_attribute_access inside pyrefly/lib/report/pysa/call_graph.rs at approximately line 2632. The LSP server and Pysa call-graph builder reuse this function to ensure consistent behavior across type-checking, "go-to-definition" navigation, and security analysis. When Pyrefly encounters a property (which is a data descriptor), it always consults the property’s methods before checking the instance __dict__.
Property Type Inference in Practice
For properties lacking explicit annotations, Pyrefly’s solver phase (pyrefly/lib/solver) propagates inferred types through the data-flow graph. If a property getter is a lambda or unannotated function, Pyrefly infers the return type from how the property is used in subsequent operations.
The following examples demonstrate how these resolution phases apply to real Python code:
# Static resolution from class annotations
class Point:
x: int
y: int
p = Point()
reveal_type(p.x) # int: resolved via static MRO lookup
# Property descriptor with typed getter
class Circle:
_radius: float
@property
def radius(self) -> float:
return self._radius
c = Circle()
reveal_type(c.radius) # float: extracted from property.__get__ return type
# Property with setter enforcing write types
class Counter:
_value: int = 0
@property
def value(self) -> int:
return self._value
@value.setter
def value(self, v: int) -> None:
self._value = v
ctr = Counter()
ctr.value = 42 # Valid: matches setter parameter type
ctr.value = "error" # Type error: incompatible with int
LSP Integration and Call Graph Construction
The attribute resolution engine powers Pyrefly’s language server features. In pyrefly/lib/lsp/wasm/completion.rs at line 421, the LSP completion provider invokes resolve_attribute_definition to populate autocomplete suggestions with accurate type information.
Similarly, the Pysa security analyzer reuses resolve_attribute_access in pyrefly/lib/report/pysa/call_graph.rs to build call graphs that include attribute-based method calls. This ensures that taint analysis correctly tracks flows through properties and dynamic attributes defined via __getattr__.
Summary
- Static resolution via
resolve_attribute_definition(line 1485 inlsp_attributes.rs) walks the MRO to locate attributes in the class hierarchy. - Dynamic fallback activates when
__getattr__or__getattribute__is present, using the dunder method’s return type as the attribute type. - Descriptor protocol handling in
resolve_descriptorextracts read and write types from property getters and setters. - Data vs. non-data descriptor precedence is enforced in
resolve_attribute_access(line 2632 incall_graph.rs), ensuring properties override instance dictionary values. - Type inference for unannotated properties occurs in the solver phase, propagating types through usage patterns.
- LSP features rely on the same resolution engine, providing consistent type information across editors and security analysis tools.
Frequently Asked Questions
How does Pyrefly handle properties without type annotations?
When a property getter lacks a return annotation, Pyrefly’s solver infers the type from the function’s return statements and usage context. If the setter lacks annotations, Pyrefly attempts to infer the write type from assigned values, though this may result in broader types or Any if insufficient context exists.
What is the difference between __getattr__ and __getattribute__ in Pyrefly's analysis?
Pyrefly treats __getattribute__ as the primary attribute access hook that intercepts all attribute lookups, while __getattr__ serves as a fallback only when normal lookup fails. If __getattribute__ is defined, Pyrefly assumes all attribute accesses are handled by this method; if only __getattr__ is present, Pyrefly first attempts static resolution before falling back to the dynamic method’s return type.
How does Pyrefly distinguish between data and non-data descriptors?
Pyrefly checks for the presence of both __get__ and either __set__ or __delete__ to classify a descriptor as a data descriptor. If only __get__ is present, it is treated as a non-data descriptor. This classification determines whether the descriptor’s value takes precedence over instance dictionary entries during attribute access resolution.
Can Pyrefly infer types for dynamically added attributes?
Pyrefly can infer types for attributes added dynamically only if the class defines __getattr__ or __setattr__ with appropriate return type annotations. For arbitrary runtime attribute assignment (e.g., obj.new_attr = value), Pyrefly typically treats the attribute as missing or falling back to Any, depending on the configuration and presence of __dict__ manipulation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →