Python Descriptors: Shallow vs Deep Resolution
This article explains the difference between shallow and deep
attribute resolution when working with Python descriptors. It covers how
Python's attribute lookup mechanism processes descriptor objects,
contrasting direct, local namespace inspection (shallow resolution) with
the full Method Resolution Order (MRO) traversal governed by
__getattribute__ (deep resolution).
Understanding Python Descriptors
A descriptor is a Python object that defines at least one of the
protocol methods: __get__(), __set__(), or
__delete__(). When an attribute defining these methods
resides on an object's class, accessing the attribute via dotted syntax
(e.g., obj.attr) invokes the corresponding protocol method
rather than directly reading or writing a dictionary entry.
Shallow Attribute Resolution
Shallow attribute resolution refers to querying an attribute directly from an object's local namespace without invoking the descriptor protocol or traversing the inheritance chain.
In shallow resolution:
- The lookup targets an explicit namespace directly, such as using
obj.__dict__['attr']orClass.__dict__['attr']. - Descriptor protocol methods like
__get__()are not executed. Accessing a descriptor viaClass.__dict__['attr']returns the raw descriptor instance itself rather than its computed value. - Python does not inspect the class hierarchy or the Method Resolution
Order (MRO). If the key does not exist in the immediate dictionary being
inspected, a
KeyErroris raised. - Shallow resolution is typically used for serialization, dynamic introspection, low-level debugging, or intentionally bypassing descriptor logic to inspect or modify underlying state.
Deep Attribute Resolution
Deep attribute resolution is the standard resolution process
performed by Python's default object.__getattribute__()
method whenever dotted notation (obj.attr) or
getattr(obj, 'attr') is used.
Deep resolution follows a strict precedence algorithm across multiple namespaces:
- Data Descriptors: Python searches the class and its
base classes via the MRO for a data descriptor (a descriptor defining
both
__get__()and either__set__()or__delete__()). If found, its__get__()method is invoked. - Instance Dictionary: If no data descriptor is
found, Python checks the instance namespace (
obj.__dict__). If present, the stored value is returned directly. - Non-Data Descriptors and Class Attributes: If the
attribute is not in
obj.__dict__, Python checks the class hierarchy again for non-data descriptors (defining only__get__()) or standard class variables. If a non-data descriptor is found, its__get__()method is executed. - Fallback (
__getattr__): If deep resolution fails to locate the attribute through the MRO and instance dictionary, Python invokes__getattr__(), if defined.
Key Differences
| Feature | Shallow Resolution | Deep Resolution |
|---|---|---|
| Lookup Mechanism | Direct mapping access
(__dict__) |
__getattribute__() via dotted
access (obj.attr) |
| Descriptor Execution | Does not execute __get__();
returns descriptor object |
Executes __get__() and
returns computed result |
| Hierarchy Traversal | Restricted to the single queried namespace | Traverses the full MRO chain |
| Precedence Handling | None; returns the exact key in the given dictionary | Prioritizes data descriptors over instance dictionaries |
| Primary Use Case | Introspection, debugging, bypassing handlers | Normal application runtime and property evaluation |
Shallow resolution treats attributes as static keys within isolated dictionaries, whereas deep resolution enforces descriptor binding behavior and inheritance rules across the entire object model.