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 accessingself.anythinginside it, causing infinite recursion.