Properties, Descriptors & The Attribute Lookup Cascade
Attribute access in Python (obj.attr) is governed by an explicit 6-step lookup cascade managed by __getattribute__(). Understanding the descriptor protocol (__get__, __set__, __delete__), the difference between Data Descriptors and Non-Data Descriptors, @property getters/setters, and __getattr__ fallback hooks is essential for mastering Python metaprogramming and framework development.
This chapter details the 6-step attribute lookup algorithm, Data vs Non-Data descriptor precedence, @property implementation mechanics, and __getattr__ vs __getattribute__.
1. The CPython 6-Step Attribute Lookup Algorithm
When Python executes obj.attr, CPython calls type(obj).__getattribute__(obj, "attr"). The lookup proceeds through 6 strict steps:
The CPython 6-Step Attribute Lookup Cascade:
[ Call: obj.attr ]
|
v
[ Step 1: Search Class Hierarchy (MRO) for Descriptor ]
|
+---> Found in Class MRO?
| βββ YES & is DATA DESCRIPTOR (defines __get__ AND __set__)?
| β βββ Call Descriptor.__get__(obj, type(obj)) IMMEDIATELY! (Step 2: Highest Precedence!)
| βββ NO (or Non-Data Descriptor): Continue to Step 3...
v
[ Step 3: Search Instance Dictionary (obj.__dict__) ]
|
+---> Found in obj.__dict__?
| βββ YES: Return obj.__dict__["attr"]!
| βββ NO: Continue to Step 4...
v
[ Step 4: Search Non-Data Descriptor in Class MRO ]
|
+---> Found in Class MRO & is NON-DATA DESCRIPTOR (defines ONLY __get__)?
| βββ YES: Call Descriptor.__get__(obj, type(obj))!
| βββ NO: Continue to Step 5...
v
[ Step 5: Search Standard Class Attribute in MRO ]
|
+---> Found in Class dict?
| βββ YES: Return Class.__dict__["attr"]!
| βββ NO: Continue to Step 6...
v
[ Step 6: Fallback to __getattr__ ]
|
+---> Is __getattr__ defined on Class?
βββ YES: Call obj.__getattr__("attr")!
βββ NO: Raise AttributeError!2. Data Descriptors vs. Non-Data Descriptors
A Descriptor is any object that implements at least one of __get__(), __set__(), or __delete__().
- Data Descriptor: Implements
__set__()or__delete__()(and optionally__get__()). Data descriptors override instance dictionaries! Even ifobj.__dict__["x"]contains a value,DataDescriptor.__get__()takes precedence. - Non-Data Descriptor: Implements ONLY
__get__()(e.g. standard functions,@classmethod,@staticmethod). Instance dictionaries take precedence over non-data descriptors.
class DataDescriptor:
def __get__(self, instance, owner):
return instance._val
def __set__(self, instance, value):
if value < 0:
raise ValueError("Must be positive")
instance._val = value
class Account:
balance = DataDescriptor() # Data Descriptor attached to class!3. @property Mechanics
The built-in @property decorator is simply a Data Descriptor implemented in C!
class Temperature:
def __init__(self, celsius: float):
self._celsius = celsius
@property
def fahrenheit(self) -> float:
return (self._celsius * 9 / 5) + 32
@fahrenheit.setter
def fahrenheit(self, value: float):
self._celsius = (value - 32) * 5 / 9When you define @property, it creates a property descriptor object with getter, setter, and deleter function pointers. Writing temp.fahrenheit = 100 calls property.__set__(), which invokes the decorated setter function.
4. __getattr__ vs. __getattribute__
__getattribute__(self, name): Called unconditionally for every attribute access on an instance. Overriding this requires callingsuper().__getattribute__(name)to avoid infinite recursion loops.__getattr__(self, name): Called only as a fallback (Step 6) when the attribute is not found in the class descriptor tree or instance__dict__. Ideal for dynamic proxying or lazy loading.