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 if obj.__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 / 9

When 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 calling super().__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.
Display Options
Appearance
Text Size
100%