Command Palette

Search for a command to run...

Hectal
PHASE 13Advanced ~28 min· topic 4 of 9

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 self is 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.

main.pywhole filepython
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__])
Instance first, then the class, then the parentsdiagram
Rendering diagram…

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.

main.pywhole filepython
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 None

05__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. 1

    Delete the shadow

    In the first example add del d.legs and print d.legs. The instance entry is gone and the class value shows through again. Predict the number first.

  2. 2

    Count the bound methods

    Add m3 = Animal.speak.__get__(d, Animal) and print m3 == m1 and m3.__func__ is Animal.speak. You have just done by hand what d.speak does for you: called the function's __get__. Then explain why m3 == m1 is False.

  3. 3

    Make a read-only class

    In Traced.__setattr__, raise AttributeError when name is not "real". Which assignment in __init__ still works and which line now fails?

Code & diagrams

Lookup order, shadowing and bound methods New tab
Sign in to run this example in your browser.

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 == Woof
The three hooks: __getattribute__, __getattr__, __setattr__ New tab

Remove the underscore check in `Config.__getattr__` and see what happens in Break it.

Sign in to run this example in your browser.

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'
A client that builds URLs from attribute names New tab

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.

Sign in to run this example in your browser.

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__: TypeError

Break 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.

main.pywhole filepython
class Broken:
    def __getattr__(self, name):
        return self._data[name]


b = Broken()
print(b.host)
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 7, in <module>
print(b.host)
^^^^^^
File "main.py", line 3, in __getattr__
return self._data[name]
^^^^^^^^^^
File "main.py", line 3, in __getattr__
return self._data[name]
^^^^^^^^^^
File "main.py", line 3, in __getattr__
return self._data[name]
^^^^^^^^^^
[Previous line repeated 990 more times]
RecursionError: maximum recursion depth exceeded

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.x on 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 make len(proxy) work by adding __len__ to one instance, and why proxy classes must define the special methods they want to forward. An explicit x.__len__() does go through the normal lookup.

  • ▸

    hasattr(obj, name) and getattr(obj, name, default) swallow AttributeError raised from anywhere in the lookup, including from inside a property's getter. A bug inside a property (an AttributeError from 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. 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. property is 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. 2

    (1) Find name on 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 raise AttributeError.

  3. 3

    Writing obj.name = value is 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. So d.legs = 3 creates an instance attribute that shadows the class attribute legs, and Animal.legs stays 4.

  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 as self. A new bound method object is made each time you read obj.method, which is why obj.method is obj.method is False, but both compare equal and share the same __func__.

  5. 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 call super().__getattribute__ inside it to avoid infinite recursion.

  6. 6

    __setattr__ runs for every assignment, including those inside __init__. Inside it, write with super().__setattr__(name, value) (or object.__setattr__). Writing self.name = value there calls __setattr__ again and loops forever. Frozen classes and validators use this hook; dataclass(frozen=True) is built on it.

  7. 7

    Two practical consequences. First, hasattr(obj, name) is only 'does the lookup succeed?', and a permissive __getattr__ makes it always True. 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

01

Describe the order in which Python looks up obj.name.

02

What is the difference between __getattr__ and __getattribute__?

03

Why is obj.method is obj.method False?

04

What happens when you assign obj.x = 5 and the class already has x = 1?

Practice

01

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.

02

Write a class Frozen that accepts x in __init__ but refuses every later assignment by overriding __setattr__. Show that f.x = 4 raises AttributeError.

03

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.m is False.

  • Done when you can choose between __getattr__ and __getattribute__.

  • Done when you can write a __setattr__ without infinite recursion.