Visual walkthrough — Inheritance — single inheritance, method resolution order (MRO)
We will only assume you know: a class is a labelled box of recipes (methods), and one class can say "I'm based on another." Everything else we build here.
Parent note: Inheritance — single inheritance & MRO.
Step 1 — A class is a box; a method is a recipe inside it
WHAT. Draw one class as a box. Inside the box lives a small dictionary called
__dict__— literally a name → recipe table. Only the recipes written in that exact class live in its own box.
WHY. Before we can talk about searching for a method, we must be precise about where a method physically lives. It does not live "somewhere in the object"; it lives in the class box that defined it. This distinction is the whole game later.
PICTURE. The box labelled
Aholds one entry:who → "return 'A'". That entry is inA.__dict__, nowhere else.

Read this as: "is the text name a key in A's own recipe table?" — name is the method name we're hunting (like who), and A.__dict__ is A's own box. Nothing about parents yet.
Step 2 — "is-a" draws an arrow to a parent box
WHAT. When we write
class B(A):, we draw an arrow fromBup toA.Bgets its own box (maybe empty ofwho), and a link saying "my base isA."
WHY. We need a way to reuse
A's recipes without copying them intoB. The arrow is that reuse: it says "if you can't find a recipe in me, there's a place to look next." Copying would duplicate bugs; the arrow keeps one source of truth.
PICTURE. Two boxes.
Bon the bottom (specific),Aon top (general), an arrowB → A.B's own box is empty;A's box haswho.

Step 3 — Every chain ends at the same root: object
WHAT. Follow the arrows up. In Python, if a class names no base, its base is secretly
object. So every chain, however long, terminates at the single box called ==object==.
WHY. We want lookup to always finish, never loop or fall off the edge. A guaranteed final box gives us a stopping point and a home for universal recipes (like how to print an object). Without a root, "search all ancestors" would have no defined end.
PICTURE. The chain
C → B → A → object.objectsits at the very top with a flat "floor" under it — the search cannot go higher.

Step 4 — Flatten the chain into a single ordered list: the MRO
WHAT. Take the arrow-chain and write it out as a flat list, front to back: most specific first,
objectlast. That ordered sequence is the Method Resolution Order — the MRO. Python actually stores it for you on the class, under the name__mro__.
WHY. Searching a tree by wandering can give ambiguous answers. Searching a sequence is unambiguous: you go left to right, you stop at the first hit, done. Python computes this order once, when the class is created, and caches it in
__mro__so every later lookup is fast and — crucially — always gives the same answer.
PICTURE. The vertical chain from Step 3 laid down horizontally as ordered slots: position 0 =
C, 1 =B, 2 =A, 3 =object.

For chains with only one parent each, this list is literally the arrow-chain read top-down. (For diamonds with multiple parents, the ordering rule generalizes to C3 linearization — out of scope here, but it reduces to exactly this list when each class has one parent.)
Step 5 — The lookup algorithm: check the instance, then walk the list
WHAT. To resolve
obj.who(): first peek inside the object's own table,obj.__dict__, in case the value was stored right on the instance. If it's not there, take the MRO from Step 4 and, for each box in order, ask Step 1's question "iswhoin this box's__dict__?". The first box that answers yes wins; stop immediately.
WHY. An object can carry its own per-object data (
self.name = "Rex"lives inobj.__dict__, not in any class). So Python must look there first — otherwise per-instance values could never shadow class-level ones. Only after the instance comes up empty do we fall back to the class chain. For methods the instance table is usually empty, so in practice the class walk is what finds them — which is why the rest of the page focuses on it.
PICTURE. A scanner first taps the small
obj.__dict__bubble (empty forwho), then sweeps left to right over[C, B, A, object]. It skipsCandB(nowho), lights up green atA(haswho), and stops —objectis greyed out, never visited.

If the instance has nothing and no box matches, the scan runs off the end of the list → Python raises AttributeError.
Step 6 — Edge case: the child overrides, so the scan stops early
WHAT. Now give
Cits ownwho. Re-run the scan.
WHY. This is the whole point of inheritance — specialization. We must see the scanner stop at index 0 and never even glance at
A. Same MRO, different first-hit.
PICTURE. Same list
[C, B, A, object], but nowC's box holdswho. The scanner lights up green at position 0 and halts;B,A,objectare all greyed out.

class A:
def who(self): return "A"
class B(A):
pass
class C(B):
def who(self): return "C"
print(C.__mro__) # (C, B, A, object) <- an immutable tuple, built at class creation
print(C().who()) # 'C' -> C matched at index 0
print(B().who()) # 'A' -> B skipped, A matchedStep 7 — Edge case: self reopens the scan from the object's real type
WHAT. A method inherited from an ancestor calls
self.tag(). Where does that inner call look? It restarts the whole lookup rule fromtype(self)— the object's real class — not from the box where the outer code text lives.
WHY. This is polymorphism: code written in a general parent automatically calls the specialized child version. The scan always keys off the object (via its
type(self).__mro__), so an old parent method can invoke a brand-new override it never knew about.
PICTURE. Left:
X.mruns (found via the scan). Inside it,self.tag()fires a second scanner overtype(self).__mro__ = (Y, X, object). That scanner hitsY.tagfirst —Y's override — even though the calling code physically sits inX.

class X:
def m(self): return "X." + self.tag() # self is a Y!
def tag(self): return "x"
class Y(X):
def tag(self): return "y"
print(Y().m()) # 'X.y'Step 8 — Degenerate case: nothing matches → fall off the end
WHAT. Call a name that neither the instance nor any box in the list defines. The scanner reaches
object, still nothing, walks off the right end.
WHY. We claimed "first hit wins" — we owe you the "no hit" outcome so no scenario is left unshown. It is not silent; it is a loud, defined error.
PICTURE. The scanner checks
obj.__dict__(empty), then sweeps every box[C, B, A, object], all grey, and drops off the edge into a redAttributeErrormarker.

C().nope() # AttributeError: 'C' object has no attribute 'nope'The one-picture summary
Everything above is one flowchart: check the instance, then compute-and-scan the __mro__ tuple, stop at first hit (or fall off the end).

Reveal the core facts:
What MRO stands for
MRO of class C(B), class B(A)
(C, B, A, object) — the chain, object last, stored in C.__mro__.What __mro__ is stored as
Where Python looks first for obj.name
obj.__dict__, before any class in the MRO.Where a method physically lives
mappingproxy) __dict__ of the exact class that wrote it.Which box wins a class lookup
__dict__ has the name.Inside self.foo(), the scan restarts from
type(self).__mro__ (the object's real class).No instance value and no box has the name →
AttributeError.Recall 🧒 Feynman: the whole walkthrough in plain words
Every class is a box of recipes (its __dict__). When one class is "based on" another, we draw an arrow upward to the more general box, and every chain of arrows ends at the same top box, object. Now flatten that chain of arrows into a plain sequence, most-specific box first, object last — Python saves that sequence for us as __mro__, a fixed tuple you can't rearrange, and "MRO" is just short for Method Resolution Order. To find obj.something, Python first peeks inside the object itself (obj.__dict__) in case the value was stored right there; if not, a little scanner walks the __mro__ tuple left to right and grabs the first box that actually has the recipe, then stops. If a child put its own version in, the child sits earlier in the tuple, so the scanner finds it first — that's overriding. When a recipe from a general box says "now do self.tag()", the scanner restarts from scratch using the object's real type list, so it can find a specialized version the general box never heard of — that's polymorphism. And if the object has nothing and the scanner walks the whole tuple and finds nothing, it falls off the end and shouts AttributeError.
Related: attribute lookup · composition over inheritance · C3 linearization.