The data model: attribute lookup

Python · Expert · 18 min

Debugging frameworks, proxies and 'impossible' attribute behaviour requires knowing the precise lookup order.

Every obj.name is a call to type(obj).__getattribute__(obj, "name"). The default implementation in object follows a precise order:

  • Search the type's MRO for name. If found and it is a data descriptor (has __set__ or __delete__), call its __get__ and return.
  • Otherwise look in the instance `__dict__`; if present, return that value.
  • Otherwise, if the class attribute was a non-data descriptor (only __get__, such as a function), call __get__; if it was a plain class attribute, return it.
  • Otherwise raise AttributeError — at which point Python calls __getattr__ if the class defines one.

So __getattr__ is a fallback for missing attributes, ideal for proxies and lazy attributes, while overriding __getattribute__ intercepts every access (and must call super().__getattribute__ to avoid infinite recursion). Assignment is simpler: a data descriptor's __set__ wins, else the value goes into the instance dict (__setattr__ can intercept).

Classes themselves are looked up through their metaclass (type.__getattribute__), which is why Class.method returns the plain function: __get__ is called with None as the instance.

Implicit special method lookup bypasses all of this. len(x), x + y and with x look the dunder up on type(x) directly, skipping the instance dict and __getattribute__. Assigning obj.__len__ = ... on an instance has no effect on len(obj).

__slots__ = ("x", "y") replaces the per-instance __dict__ with fixed storage, exposed through member descriptors on the class. Instances get smaller and attribute access slightly faster, at the cost of no ad-hoc attributes. Every subclass needs its own __slots__ or it regains a __dict__.

Example

class Lazy:
    def __getattr__(self, name):            # only called for *missing* attrs
        print(f"loading {name}")
        value = name.upper()
        setattr(self, name, value)          # cache in the instance dict
        return value

obj = Lazy()
print(obj.config)          # loading config / CONFIG
print(obj.config)          # CONFIG (found in __dict__, no fallback)
class Sized:
    def __len__(self):
        return 3
s = Sized()
s.__len__ = lambda: 99     # instance attribute is ignored by len()
print(len(s), s.__len__()) # 3 99
class Point:
    __slots__ = ("x", "y")
    def __init__(self, x, y):
        self.x, self.y = x, y
print(type(Point.__dict__["x"]).__name__)   # member_descriptor
try:
    Point(1, 2).z = 3
except AttributeError as e:
    print("no __dict__:", e)

Remember

  • Data descriptors > instance __dict__ > non-data descriptors/class attributes > __getattr__
  • __getattr__ handles only misses; __getattribute__ sees every access
  • Special methods are looked up on the type, bypassing the instance

Watch out for

  • Overriding __getattribute__ and accessing self.anything inside it, causing infinite recursion.

Check yourself

  1. A class defines `__getattr__`. When is it called for `obj.name`?
  2. Why doesn't `obj.__len__ = lambda: 5` change what `len(obj)` returns?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/reference/datamodel.html#customizing-attribute-access