Descriptors Explain Python's Attribute Lookup
A step-by-step model for data descriptors, instance dictionaries, methods, and attribute fallbacks.
Methods, properties, slots, and many ORM fields are built on the descriptor protocol. An object stored on a class is a descriptor when its type defines __get__, __set__, or __delete__.
The ordering rules explain why some class attributes override instance state while others can be shadowed.
Data descriptors come first
For a normal instance attribute read, Python conceptually checks:
- A data descriptor found on the class or its method resolution order.
- The instance dictionary.
- A non-data descriptor or ordinary class attribute.
__getattr__if the preceding lookup raisesAttributeError.
A descriptor defining __set__ or __delete__ is a data descriptor, even if those methods only raise an exception.
class Positive:
def __set_name__(self, owner, name):
self.storage_name = f'_{name}'
def __get__(self, instance, owner=None):
if instance is None:
return self
return getattr(instance, self.storage_name)
def __set__(self, instance, value):
if value <= 0:
raise ValueError('must be positive')
setattr(instance, self.storage_name, value)
Because Positive is a data descriptor, placing a same-named key in an instance’s __dict__ does not bypass it.
Functions bind through __get__
Python functions stored on a class are non-data descriptors. Access through an instance calls the function’s __get__, producing a bound method that supplies the instance as its first argument.
class Greeter:
def greet(self, name):
return f'Hello {name}'
Greeter.greet # function
Greeter().greet # bound method
Since functions are non-data descriptors, an instance attribute can shadow a method. Assigning instance.greet = callback changes lookup for that instance without changing the class.
A read-only property—one created without a setter—is also a non-data descriptor. Adding a same-named entry directly to instance.__dict__ therefore shadows the property. Supplying a setter upgrades it to data-descriptor precedence.
The receiver and owner carry context
descriptor.__get__(instance, owner) receives instance=None when accessed through the class. Descriptors commonly return themselves in that case, making metadata and configuration available to tooling.
__set_name__ runs during class creation and tells a descriptor which attribute name received it. Assigning a descriptor to a class later does not automatically repeat that hook; call it explicitly or design another setup path.
Custom lookup needs careful delegation
__getattribute__ runs for every ordinary attribute read. An override should normally delegate to super().__getattribute__ to preserve descriptors and avoid recursion. __getattr__ is a fallback invoked only after normal lookup fails.
Calling object.__getattribute__(instance, name) directly still invokes the class’s __getattr__ hook when the name is absent, because __getattr__ is part of the base lookup algorithm. It is therefore enough to implement all fallback behaviour in __getattr__ without considering the caller.
Descriptors are less magical when treated as one ordered lookup protocol. The crucial questions are where the descriptor lives, whether it is data or non-data, and which object is being used as the receiver.