Descriptors and Attribute Magic
Take control of what obj.attr really does
Big Idea: Every time you write
obj.attr, Python runs a small algorithm to decide where that value comes from. Three mechanisms let you reshape that algorithm: the descriptor protocol (__get__/__set__/__delete__) attaches reusable get/set logic to a class attribute; __getattr__ and __getattribute__ intercept lookups on an instance; and __slots__ replaces the per-instance __dict__ with a fixed, compact layout. Master these and you understand how property, classmethod, ORMs and dataclasses are built.
By the end you will be able to:
- trace the exact order Python uses to resolve
obj.attr, - write a reusable descriptor with
__get__,__set__and__set_name__, - distinguish data from non-data descriptors and predict which wins over the instance dict,
- use
__getattr__for fallbacks and know why__getattribute__is a loaded gun, - apply
__slots__to cut memory and lock down attributes, with its trade-offs.
1. What really happens on obj.attr
Attribute access is not a simple dictionary lookup. Reading obj.attr calls type(obj).__getattribute__(obj, "attr"), and the default implementation on object follows a precise order. Understanding that order is the whole foundation of this topic.
- Python walks the type's MRO (method resolution order) looking for
attr. If it finds a class attribute that is a data descriptor (defines__set__or__delete__), that descriptor wins immediately and its__get__runs. - Otherwise Python checks the instance's own
__dict__. Ifattris there, that value is returned. - Otherwise, back to the class attribute found on the MRO: if it is a non-data descriptor (only
__get__) or a plain value, that is used. - If nothing was found, Python calls
__getattr__as a last-resort hook. If that too is absent,AttributeErroris raised.
The single most important consequence: a data descriptor on the class beats the instance dict, but a non-data descriptor loses to the instance dict. That one rule explains most "why did my attribute do that?" surprises.