How Python Resolves Attribute Lookups

When accessing an attribute in Python using dot notation (such as obj.attr), the interpreter executes a precise resolution order rather than a simple dictionary query. Python navigates through data descriptors in the class hierarchy, the instance's internal dictionary, non-data descriptors, standard class and base class attributes via the Method Resolution Order (MRO), and finally falls back to dynamic fallback methods before raising an exception.

1. Data Descriptors in the Class and Base Classes

The lookup begins not at the instance level, but at the class level. Python inspects the type of the instance (type(obj)) and traverses its base classes using the Method Resolution Order (MRO).

During this initial pass, Python checks if the attribute name corresponds to a data descriptor—an object defining a __get__() method along with either __set__() or __delete__() (such as property). If a data descriptor is found in the class or any inherited class dictionary, Python immediately invokes its __get__() method and returns the result, overriding any value that might exist on the instance itself.

2. The Instance Dictionary (__dict__)

If no data descriptor matches the attribute name, Python shifts its search to the instance itself. It queries the instance's internal __dict__ namespace:

obj.__dict__['attr']

If the key exists within obj.__dict__, Python returns that value. If the class utilizes __slots__ instead of a standard __dict__, Python checks the slot descriptor allocated for that specific instance.

3. Non-Data Descriptors and Class Attributes

If the attribute does not exist in the instance namespace, Python returns to the class and its base classes via the MRO to check their respective dictionaries (cls.__dict__). At this stage, it handles two possibilities:

4. The Fallback Mechanism (__getattr__)

If the attribute is not found in the data descriptors, the instance dictionary, or the class hierarchy, Python searches for a custom fallback method:

Overriding the Resolution Pipeline

This entire sequence is coordinated by the default implementation of object.__getattribute__(self, name). If a class overrides __getattribute__, it replaces this standard resolution algorithm entirely, requiring manual invocation of super().__getattribute__(name) to preserve default lookup behavior.