Attribute Lookup in Python

Learn the step-by-step process Python uses to resolve attribute access, from data descriptors to instance dictionaries to __getattr__ fallback.

8 min read

Attribute lookup is the process Python follows when you write obj.name to read an attribute. It is not a simple dictionary lookup. Python checks multiple sources in a specific order, and understanding that order is essential for working with descriptors, properties, and attribute customization.

The article on Python descriptors introduced the descriptor protocol. The article on customizing Python objects covered the getattr and getattribute hooks. This article connects them by walking through the full lookup chain step by step.

When you know the lookup order, you can predict which hook will fire, understand why a property takes priority over an instance variable, and debug situations where an attribute seems to come from the wrong source.

The lookup chain step by step

Python follows this exact sequence for every obj.name access. Each step either finds the attribute or passes control to the next step.

Step one checks for a data descriptor on the type, which implements both the get method and either the set or delete method. If found, Python calls the descriptor's get method with the instance and owner. This step is why properties always work even when an instance attribute shadows the name.

Step two checks the instance dictionary. If the name is a key in the instance dictionary, Python returns the value directly. This step only runs when no data descriptor was found.

Step three checks for a non-data descriptor or a plain class attribute on the type. Python walks the type's method resolution order, searching each class in the inheritance chain. Non-data descriptors implement only the get method and can be overridden by instance attributes.

If none of these steps find the attribute, Python calls the getattr method on the object as a last resort. If getattr is not defined or raises AttributeError, the original exception propagates.

Demonstrating each step in the chain

You can verify the lookup order by setting up an object with attributes at each level and seeing which one wins.

pythonpython
class DataDesc:
    def __get__(self, instance, owner):
        return "data descriptor"
    def __set__(self, instance, value):
        pass
 
class NonDataDesc:
    def __get__(self, instance, owner):
        return "non-data descriptor"
 
class Example:
    data_attr = DataDesc()
    plain_attr = NonDataDesc()
    class_attr = "class attribute"
 
obj = Example()
obj.__dict__["plain_attr"] = "instance value"
obj.__dict__["class_attr"] = "instance value"

The data_attr uses a data descriptor. The plain_attr has both a non-data descriptor on the class and an entry in the instance dictionary. The class_attr has a class attribute and an instance override.

pythonpython
print(obj.data_attr)
print(obj.plain_attr)
print(obj.class_attr)

The data descriptor returns its value regardless of anything stored in the instance dictionary. The non-data descriptor on the class is overridden by the instance attribute entry.

texttext
data descriptor
instance value
instance value

The non-data descriptor on the class lost to the instance dictionary entry. This is the key difference between data and non-data descriptors that the lookup order enforces.

How getattr fits into the chain

The getattr method is the final fallback. Python calls it only after all other lookup sources have been exhausted. This makes it ideal for providing default values, delegating to other objects, or computing values on demand.

pythonpython
class Fallback:
    def __init__(self):
        self.existing = "I exist"
 
    def __getattr__(self, name):
        return f"computed: {name}"
 
obj = Fallback()
print(obj.existing)
print(obj.missing)

The existing attribute returns normally from the instance dictionary lookup. The missing attribute, which has no entry anywhere in the chain, triggers the getattr fallback as the last resort.

texttext
I exist
computed: missing

If you need to intercept every attribute access including existing ones, override getattribute instead, and use it sparingly since it fires unconditionally.

The role of the method resolution order

When Python looks for class-level attributes in step three, it walks the method resolution order of the object's type. The MRO is the linear ordering of classes that Python follows during inheritance.

pythonpython
class A:
    value = "A"
 
class B(A):
    pass
 
class C(A):
    value = "C"
 
class D(B, C):
    pass
 
print(D.value)
print(D.__mro__)

Class D inherits from B and C. Python resolves value by walking the MRO, finding it on C before reaching A.

texttext
C
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

The article on Python method resolution order explains the C3 linearization algorithm that produces this ordering and why it guarantees consistent, predictable resolution.

Why lookup order matters in practice

The lookup order explains several behaviours that otherwise seem inconsistent.

When a property defined on a class blocks an instance attribute with the same name, it is because properties are data descriptors and step one always wins.

When a method defined on a class can be overridden by assigning a function to an instance, it is because functions are non-data descriptors and step two can shadow them.

When getattr provides a default for missing attributes but does not interfere with existing ones, it is because step four only runs when all other steps fail.

Understanding this chain lets you choose the right tool for each situation.

Need to block instance overrides? Use a data descriptor.

Want to allow per-instance customization? Use a non-data descriptor or a class attribute.

Need a catch-all for missing attributes? Implement getattr.

Rune AI

Rune AI

Key Insights

  • Python's attribute lookup follows a strict order: data descriptors, instance dict, non-data descriptors, class/MRO, getattr.
  • Data descriptors (get + set/delete) always beat instance dictionaries.
  • Non-data descriptors (get only) can be shadowed by instance attributes.
  • getattr fires only when every other lookup source has failed.
  • Understanding lookup order is essential for debugging property, descriptor, and proxy interactions.
RunePowered by Rune AI

Frequently Asked Questions

What is the order of attribute lookup in Python?

Python follows a specific order: first check for a data descriptor (has __get__ and __set__/__delete__) on the type, then check the instance __dict__, then check for a non-data descriptor or class attribute on the type and its bases via MRO, and finally call __getattr__ as a fallback. This order ensures that data descriptors like property always take priority over instance attributes.

Why does __getattr__ only fire when lookup fails?

__getattr__ is designed as a last-resort fallback. It is called only after Python has checked data descriptors, the instance dictionary, non-data descriptors, class attributes, and all parent classes through MRO. This design prevents __getattr__ from interfering with normal attribute access and keeps attribute resolution predictable and performant.

Conclusion

Python's attribute lookup is a layered, predictable chain. Data descriptors win over instance dictionaries, non-data descriptors can be shadowed, and getattr catches whatever remains. Understanding this chain lets you predict exactly which hook will fire for any attribute access, which is essential for debugging descriptor, property, and customization issues.