Topic 13.4
How Attribute Lookup Works
In one line
Reading obj.name follows a fixed search: data descriptors on the type, then the instance's __dict__, then the class and its parents in MRO order, and only then __getattr__. Methods are plain functions that become bound methods when you read them.
Think of it like this
Finding a book. You look in your own bag first. If it is not there, you check the classroom shelf. Then the school library, which has its own sections in a fixed order. If nobody has it, you ask the librarian, who may say 'I will find you something else'. Python looks up obj.name in a similar fixed order: the object itself, then its class, then the parent classes, then a last-chance helper.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Attribute
- A name attached to an object, read with
obj.name. - Instance `__dict__`
- The dictionary holding an object's own attributes.
- MRO
- Method Resolution Order: the fixed list of classes Python searches, child first, then parents.
- Shadowing
- An instance attribute hiding a class attribute with the same name.
- Bound method
- A function packaged with the instance it was read from, so
selfis filled in automatically. - `__getattribute__`
- The hook that runs on every attribute read and performs the normal lookup.
- `__getattr__`
- A fallback that runs only when the normal lookup fails.
- `__setattr__`
- The hook that runs on every attribute assignment.
Step by step
01Instance first, then the class, then the parents
In the example d.name is found in the instance dict. d.legs is not on d or on Dog, but it is on Animal, which comes next in the MRO. type(d).__mro__ shows the exact search order.
class Animal:
legs = 4
class Dog(Animal):
pass
d = Dog()
d.name = "Rex"
print(d.__dict__) # {'name': 'Rex'}
print(d.legs) # 4, found on Animal
print([c.__name__ for c in type(d).__mro__])02Writing goes to the instance
d.legs = 3 creates legs in d.__dict__. The class attribute is untouched, so Animal.legs is still 4. This also explains the shared-mutable-class-attribute trap from Topic 8.2: obj.items.append(1) reads the class list and changes it in place (no assignment happens), while obj.items = [] creates a private instance attribute.
03Methods are functions that bind on access
Dog.speak is a plain function. d.speak calls the function's __get__, which returns a bound method holding d. Calling it passes d as the first argument. Dog.speak(d) does the same by hand.
Bound methods are cheap but they are objects; CPython optimises the common call obj.method(args) so it often skips creating one.
04__getattr__ is the fallback
__getattr__ receives only names that the normal search did not find. That makes it safe for 'default or computed' attributes. Inside it, be careful with names that start with an underscore: if __getattr__ itself touches self._data before _data exists, the lookup fails, calls __getattr__ again, and recurses until Python raises RecursionError.
class Config:
def __init__(self, data):
self._data = data
def __getattr__(self, name):
if name.startswith("_"):
raise AttributeError(name)
try:
return self._data[name]
except KeyError:
raise AttributeError(f"no setting {name!r}") from None05__getattribute__ and __setattr__ see everything
The tracing class in the first runnable example prints for every read and every write. Notice the order for a missing name: first __getattribute__ runs and fails, then __getattr__ runs. Notice also that hasattr(t, "nothing") is True, because __getattr__ returned a value.
Always delegate to super() inside these hooks. Reading self.something inside __getattribute__ re-enters the hook.
06Why this matters in real libraries
ORMs build attributes from table columns, mock objects return a new mock for any name, and API clients turn client.users.list() into HTTP paths, all with __getattr__. Dataclasses, Pydantic models and property use the type-level steps. The more of this you understand, the less 'magic' any library looks.
Try it yourself
- 1
Delete the shadow
In the first example add
del d.legsand printd.legs. The instance entry is gone and the class value shows through again. Predict the number first. - 2
Count the bound methods
Add
m3 = Animal.speak.__get__(d, Animal)and printm3 == m1andm3.__func__ is Animal.speak. You have just done by hand whatd.speakdoes for you: called the function's__get__. Then explain whym3 == m1isFalse. - 3
Make a read-only class
In
Traced.__setattr__, raiseAttributeErrorwhennameis not"real". Which assignment in__init__still works and which line now fails?
Code & diagrams
Expected output
instance dict: {'name': 'Rex'}
name -> Rex (found in the instance dict)
legs -> 4 (not on the instance or Dog, found on Animal)
MRO -> ['Dog', 'Animal', 'object']
after d.legs = 3: 3 4 {'name': 'Rex', 'legs': 3}
bound method is new each time: False
but they are equal: True
underlying function is shared: True
bound to: True
called by hand: Woof == WoofRemove the underscore check in `Config.__getattr__` and see what happens in Break it.
Expected output
__setattr__: real = 1
read real:
__getattribute__: real
1
read missing:
__getattribute__: missing
__getattr__: missing
default-missing
write:
__setattr__: extra = 5
hasattr uses the same path:
__getattribute__: nothing
__getattr__: nothing
True
localhost 8080
AttributeError: no setting 'timeout'This is the pattern behind many API clients: every public name the lookup cannot find becomes a deeper path. Special-method calls such as `len(api)` skip `__getattr__` completely.
Expected output
GET https://api.example.test/users/list
GET https://api.example.test/users/list?active=true&limit=10
GET https://api.example.test/invoices/paid/summary?year=2025
True
False
len() looks on the class, not through __getattr__: TypeErrorBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Recursive __getattr__
Write a __getattr__ that reads an attribute that was never set.
class Broken:
def __getattr__(self, name):
return self._data[name]
b = Broken()
print(b.host)Myth vs fact
Myth
__getattr__ is called every time you read an attribute.
Fact
It is only a fallback after the normal search fails. __getattribute__ is the one that runs every time.
Myth
Methods are stored on each instance.
Fact
There is one function per class. Reading obj.method makes a small bound-method object that pairs the function with obj.
Myth
Assigning to obj.x changes the class attribute x.
Fact
Assignment goes to the instance's __dict__ (unless a data descriptor intercepts it), so it shadows the class attribute.
Myth
hasattr checks that an attribute really exists.
Fact
It only checks that reading it does not raise AttributeError. A forgiving __getattr__ makes it True for any name.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
CPython speeds up attribute access with the specialising interpreter (Topic 13.3): it caches the type's version and the position of the attribute, so a repeated
obj.xon the same shape of object is close to an array index. Changing a class at run time invalidates those caches. - ▸
Instances usually store attributes in key-sharing dictionaries (and, in recent versions, in inline values) so thousands of objects of one class share their key layout. That is why creating attributes in the same order in
__init__matters for memory, and why__slots__(Topic 8.12) saves still more. - ▸
object.__getattribute__(obj, name)is the escape hatch to the normal lookup. Proxy and mock libraries use it to reach their own fields without triggering their own hooks. - ▸
Implicit calls of special methods (
len(x),x + y,for a in x,with x:) are looked up on the type, never on the instance and never through__getattr__or__getattribute__. That is why you cannot makelen(proxy)work by adding__len__to one instance, and why proxy classes must define the special methods they want to forward. An explicitx.__len__()does go through the normal lookup. - ▸
hasattr(obj, name)andgetattr(obj, name, default)swallowAttributeErrorraised from anywhere in the lookup, including from inside a property's getter. A bug inside aproperty(anAttributeErrorfrom a typo) can therefore look like 'the attribute does not exist', and with a__getattr__fallback the real error is replaced by a confusing one. When debugging, read the property's code, not just the message. - ▸
Be sparing with
__getattr__in AI and data libraries: a lazy attribute that makes a network call turns every typo into a slow request, and static analysers (type checkers, autocompletion) cannot see attributes that appear only through__getattr__.
Remember this
- 1
The order, in plain terms, for reading
obj.name, is below. A descriptor is an object stored on a class that runs its own code when you read or write the attribute.propertyis the one you know (Topic 8.4), and Topic 13.5 builds them. A data descriptor also defines a write or delete step (__set__or__delete__). - 2
(1) Find
nameon the object's type by walking the MRO (type(obj).__mro__, Topic 8.6). (2) If what you found is a data descriptor, use it. (3) Otherwise look in the instance's own__dict__. (4) Otherwise use what was found on the type: a plain value, or a non-data descriptor such as a function. (5) If nothing was found, call__getattr__if the class defines it. (6) Otherwise raiseAttributeError. - 3
Writing
obj.name = valueis simpler: Python checks the type for a data descriptor with__set__(such as a property) and uses it; otherwise it stores the value in the instance's__dict__. It never writes to the class. Sod.legs = 3creates an instance attribute that shadows the class attributelegs, andAnimal.legsstays 4. - 4
A method is just a function stored on the class. Functions are non-data descriptors: their
__get__returns a bound method that remembers the instance and passes it asself. A new bound method object is made each time you readobj.method, which is whyobj.method is obj.methodisFalse, but both compare equal and share the same__func__. - 5
__getattribute__is the hook that runs for every attribute read; it is where the whole search above lives (object.__getattribute__).__getattr__is different: it is only a fallback that runs after the normal search fails. Define__getattr__for 'compute or default missing attributes' (proxies that forward to another object, config objects, lazy loading that waits until first use). Override__getattribute__only for tracing or very special cases, and callsuper().__getattribute__inside it to avoid infinite recursion. - 6
__setattr__runs for every assignment, including those inside__init__. Inside it, write withsuper().__setattr__(name, value)(orobject.__setattr__). Writingself.name = valuethere calls__setattr__again and loops forever. Frozen classes and validators use this hook;dataclass(frozen=True)is built on it. - 7
Two practical consequences. First,
hasattr(obj, name)is only 'does the lookup succeed?', and a permissive__getattr__makes it alwaysTrue. Second,__dict__is a real dict:vars(obj)shows an instance's own attributes, and classes with__slots__(Topic 8.12) have no per-instance dict at all, which changes step 3 of the search.
Explain it without notes
Describe the order in which Python looks up obj.name.
What is the difference between __getattr__ and __getattribute__?
Why is obj.method is obj.method False?
What happens when you assign obj.x = 5 and the class already has x = 1?
Practice
Write a class Lenient with a class attribute shade = "blue" and a __getattr__ that returns "?" for any missing attribute. Print o.shade and o.colour. Then set o.shade = "red", print o.shade and Lenient.shade, delete o.shade and print it again.
Write a class Frozen that accepts x in __init__ but refuses every later assignment by overriding __setattr__. Show that f.x = 4 raises AttributeError.
Using Config from this topic, show that cfg.host works and cfg.timeout raises AttributeError.
Trade-offs
- ↔
Dynamic attributes are flexible but hide errors.
__getattr__makes proxies and config objects pleasant to use, but a typo may become a silent default and tools cannot check it. - ↔
Hooks cost speed. An override of
__getattribute__or__setattr__is run in Python on every access, which is far slower than the built-in C path. Use them only where the behaviour is needed.
Done when you can
Done when you can list the lookup order for reading an attribute from memory.
Done when you can explain why assignment shadows a class attribute instead of changing it.
Done when you can explain bound methods and why
obj.m is obj.mis False.Done when you can choose between
__getattr__and__getattribute__.Done when you can write a
__setattr__without infinite recursion.