Python Attribute Lookup
Attribute access in Python follows a defined lookup process involving the instance, its class, descriptors, and the inheritance hierarchy. Understanding that process explains why `obj.x` can return an instance value, invoke a descriptor, or fall back to a class or base class.
When Python evaluates `obj.x`, it normally enters `type(obj).__getattribute__(obj, "x")`. The lookup logic searches the class hierarchy and considers descriptors before deciding whether an instance dictionary or class attribute supplies the value.
Descriptors are objects defining methods such as `__get__`, `__set__`, or `__delete__`. A data descriptor, which defines `__set__` or `__delete__`, takes precedence over an instance attribute of the same name. This is why properties can control access even when an instance dictionary contains a matching key.
If no data descriptor wins, Python checks the instance namespace, typically `obj.__dict__`. If the name is found there, that value is returned. Otherwise, Python can use a non-data descriptor or an ordinary attribute found on the class or one of its base classes.
The inheritance search follows the class's method resolution order, or MRO. If the normal lookup mechanism does not find the attribute, an implementation can raise `AttributeError`, after which `__getattr__` may provide a fallback. Overriding `__getattribute__` can intercept every attribute access and therefore requires care to avoid accidental infinite recursion.
Key points
- `__getattribute__` is the main hook for normal attribute access.
- Data descriptors take precedence over an instance attribute with the same name.
- The instance namespace is considered before non-data descriptors and ordinary class attributes.
- Class lookup follows the MRO, including inherited attributes.
- `__getattr__` is a fallback after ordinary lookup fails.
Interview questions
What happens when Python evaluates `obj.attr`?
Python normally routes the access through `type(obj).__getattribute__`. The lookup process considers descriptors and the instance namespace, then searches class attributes according to the MRO. If the name still cannot be found, `AttributeError` results unless a fallback such as `__getattr__` handles it.
Why can a property override a value stored in an instance's `__dict__`?
A typical `property` is a data descriptor because it defines descriptor methods including `__set__` or `__delete__` behavior. Data descriptors have higher precedence than the instance dictionary during attribute lookup. Therefore, a matching key in `obj.__dict__` does not normally override the property's access behavior.
What is the difference between `__getattribute__` and `__getattr__`?
`__getattribute__` is invoked for normal attribute access and can therefore intercept essentially every lookup on an object. `__getattr__` is only consulted when normal lookup raises `AttributeError`. A common use of `__getattr__` is providing dynamic fallback attributes without replacing the entire normal lookup mechanism.
Explain the precedence between a data descriptor, instance attribute, and non-data descriptor.
For an ordinary instance lookup, a data descriptor found through the class hierarchy has priority over the instance dictionary. If no data descriptor supplies the attribute, Python can return a value from the instance dictionary. If that also fails, a non-data descriptor or ordinary class attribute can supply the value. This precedence is a key part of Python's descriptor-based object model.
What risk exists when overriding `__getattribute__`, and how can you avoid it?
Because `__getattribute__` handles normal attribute access, accessing another attribute through `self` inside the override can invoke the override again. That can cause infinite recursion. A common technique is to delegate ordinary access to `object.__getattribute__(self, name)` rather than recursively using `self.name`. The override should also be kept narrow because it changes a fundamental part of object behavior.