Python Attribute Lookup
In an effort to understand the very basics, I’ve been implementing classes in Python from scratch, starting with the functions and dictionaries. I was curious how to implement attribute lookup how python actually does it. So I went down a rabbit hole and came across this article that covers it exhaustively. Learned a great deal about how alot of the higher level ORMS, frameworks like FastAPI and dataclasses are implemented.
Some key takeaways are:
-
Instance variables take precedence over class variables.
-
class Foo(): x = 'Foo class attribute' x = Foo() x.name
From above, x.name essentially does a
x.__get__attribute("name")which checks in the instance’s attribute dict first, doesnt find it, then checks in theclass.__dict__which it does find. -
-
data descriptors take precedece over instance, class and non-data descriptors.
You can also have things called descriptors. These are basically just objects that change how you access the different object attributes. There’s two:
- a) Data descriptors
- b) Non-data descriptors.
Data descriptors have __set__, __delete__ and __get__ methods implemented while non-data descriptors only have __get__ implemented. The easiest example of this is the @property object. Under the hood, it’s basically a data descriptor.
The data descriptor is the first thing that Python looks up when trying to get an attribute. These property descriptors also override an instance’s attributes, non-data descriptors, and plain class attributes.
To illustrate this, here’s an example:
class Foo:
@property
def x(self):
return "computed"
obj = Foo()
obj.__dict__["x"] = "shadow?"
obj.x
This will actually return computed and not shadow because during @property is a data descriptor, thus Python will look at it’s __set__ or __get__ methods first which x is. Thus it will return before we saw the instance obj attribute shadow.
So the general approach is:
-
Data descriptor (
property) -
Instance
__dict__ -
Non‑data descriptor
-
Class attribute
-
MRO
-
__getattr__In the case of where you have multiple inheritances, python uses Method resolution Order to access the attribute, specifically the C3 linearization algorithm.
Borrowing an image and example from the above article:
class A(object): pass
class B(object):
x = x from B
class C(A, B):pass
class D(B):
x = x from D
class E(C, D): pass
![[Pasted image 20260301141019.png]]
>>> E.__mro__
(__main__.E, __main__.C, __main__.A, __main__.D, __main
C3 linearization produces an MRO that satisfies three constraints:
1. Local precedence order
If a class lists bases as class C(A, B), then A must appear before B in the MRO of C.
2. Monotonicity
A subclass’s MRO must preserve the order of its parents’ MROs. This prevents “jumps” that would break inheritance consistency.
3. No contradictions
If two parents disagree on ordering, Python must find a consistent merge or raise an error.
This has made it so much easier to understand what frameworks like FastAPI and ORMs do behind the scenes.