Attribute Lookup Chain — Core Concepts
The Full Lookup Order
When you write obj.attr in Python, the interpreter follows a precise sequence to find the value. This isn’t a simple parent-child search — there are five steps, and the order matters:
- Data descriptors on the class (and its parents) — checked first.
- Instance
__dict__— the object’s own attributes. - Non-data descriptors and other class attributes — methods, class variables.
__getattr__— a fallback hook if nothing was found.AttributeError— if none of the above found anything.
What Are Descriptors?
A descriptor is any object that defines __get__, __set__, or __delete__. They live on the class, not the instance, and they intercept attribute access.
There are two kinds:
- Data descriptors: Define both
__get__and__set__(or__delete__). They take priority over instance attributes. - Non-data descriptors: Define only
__get__. Instance attributes override them.
The Most Common Descriptors
| Descriptor | Type | You Know It As |
|---|---|---|
property | Data | @property decorator |
| Functions | Non-data | Regular methods |
classmethod | Non-data | @classmethod |
staticmethod | Non-data | @staticmethod |
__slots__ entries | Data | Memory-optimized attributes |
Why Data Descriptors Win
The @property decorator creates a data descriptor. When you access obj.attr and there’s a property named attr on the class, the property’s __get__ method runs — even if obj.__dict__ also has an attr key.
This is by design. Properties are meant to control access to an attribute. If instance attributes could override them, properties would be unreliable.
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
Here, radius is a data descriptor. Even if you sneak a radius key into obj.__dict__, the property still intercepts access.
Instance Dict vs. Class Dict
Every regular Python object has a __dict__ — a dictionary holding its attributes:
class Dog:
species = "Canis familiaris" # Class attribute (in Dog.__dict__)
def __init__(self, name):
self.name = name # Instance attribute (in self.__dict__)
Dog.__dict__containsspeciesand__init__.fido.__dict__containsname.fido.speciesworks because after checkingfido.__dict__and not findingspecies, Python checksDog.__dict__.
The __getattr__ Fallback
If the normal lookup fails to find the attribute, Python calls __getattr__ (if defined). This is your last chance to provide a value:
class Flexible:
def __getattr__(self, name):
if name.startswith("get_"):
field = name[4:]
return lambda: self.__dict__.get(field, None)
raise AttributeError(f"No attribute '{name}'")
Important: __getattr__ only runs when normal lookup fails. There’s also __getattribute__, which runs on every attribute access — but overriding it is risky and rarely needed.
Common Misconception
“Instance attributes always win.” They don’t. Data descriptors (properties, slots) beat instance attributes. This surprises developers who set obj.__dict__['attr'] directly and find that a property still intercepts their access.
Quick Reference: The Lookup in Order
| Step | Checked | Example |
|---|---|---|
| 1 | Data descriptor on class/parents | @property, __slots__ |
| 2 | Instance __dict__ | self.x = 10 |
| 3 | Non-data descriptor / class attr | Methods, @classmethod, class variables |
| 4 | __getattr__ | Custom fallback |
| 5 | AttributeError | Nothing found |
Practical Implications
- Setting an attribute with
obj.attr = valuegoes intoobj.__dict__unless a data descriptor’s__set__intercepts it. - Deleting an attribute with
del obj.attrremoves it fromobj.__dict__unless a data descriptor’s__delete__intercepts it. hasattr(obj, 'x')follows the full lookup chain, including__getattr__.
One thing to remember: Python’s attribute lookup is a five-step process where data descriptors beat instance attributes, instance attributes beat class attributes, and __getattr__ is the last resort before an error.
See Also
- Python Bytecode And Interpreter How your .py file turns into tiny instructions the Python interpreter can execute step by step.
- Python Class Body Execution Python runs the code inside your class definition immediately — like reading a recipe out loud before anyone starts cooking.
- Python Data Model Customization How Python lets your objects behave like built-in types — adding, comparing, looping, and printing, all with special methods.
- Python Garbage Collection See how Python cleans up unreachable objects, especially the tricky ones that point at each other.
- Python Gil Why Python threads can feel stuck in traffic, and how the GIL explains the behavior.