Before solving anything, let us name every distinct case a single-inheritance / MRO question can be. Each row is a "cell"; every worked example below is tagged with the cell it fills.
#
Case (the "quadrant")
What makes it different
Example
1
Direct hit
The method lives in the object's own class
WE-1
2
Inherited hit
Child empty, name found up the chain
WE-2
3
Two overrides, dynamic dispatch
Inherited method calls another method that is overridden — type(self) decides
WE-3
4
super() to extend
Run parent's version then add
WE-4
5
Deep chain (3+ levels)
Which of several ancestors wins
WE-5
6
Degenerate: name nowhere
Nobody defines it → AttributeError
WE-6
7
Degenerate: attribute masks a method
A data attribute shadows a method of the same name → calling it raises TypeError
Every single example uses one algorithm. Keep this picture in your head:
How to read Figure s01. The purple box at the top is the object's own __dict__ (its personal storage). The row of boxes below is the class chain — the MRO — with the child on the left (orange), ancestors in teal, and object (the root) at the far right in dark ink. The purple arrow shows lookup checking the instance first; the horizontal arrows show it then walking the class chain left-to-right. The orange line at the bottom is the stop rule; the ink line is the failure rule. Every later figure is just this same walk applied to a specific example.
MRO for single inheritance is just the parent chain ending in object, most-specific first.
How to read Figure s02. Same walk as s01, applied here. The orange Square box (leftmost in the MRO) is checked first; because area is in Square.__dict__, the bold orange ring marks it as the stop point and the teal Shape box is never reached. This is the shortest possible walk — one step.
Steps
type(obj) is Square, so MRO is (Square, Shape, object).
Why this step? Lookup always starts from the object's dynamic type, never from where we happen to be reading.
Walk left→right: is area in Square.__dict__? Yes. Stop immediately.
Why this step? The first match wins; we never look at Shape.
Run Square.area(self) with self.s = 4 → 4 * 4 = 16.
Why this step?self.s was set in __init__, so the multiply is well-defined.
Recall Answer
16. Direct hit — the child's own area is found first.
Verify: side 4⇒ area =42=16. Units check: a length² gives an area. ✓
How to read Figure s03. Same walk. The orange Circle box has no area (labelled "none"), so the ink arrow steps right to the teal Shape box, where area lives — that box gets the orange stop ring. The walk takes two steps instead of one, illustrating "inheritance fills the gap."
Steps
MRO is (Circle, Shape, object).
Why this step? Single parent chain + root.
area in Circle.__dict__? No (pass = empty body). Keep walking.
Why this step? An empty class just forwards every lookup upward.
area in Shape.__dict__? Yes → run Shape.area → returns 0.
Why this step? Inheritance means "borrow the parent's version when you have none."
Recall Answer
0. Not a crash — the inherited method fills the gap.
How to read Figure s04. The orange box on the left is our object, Duck(), carried around as self. The teal box (top right) is Bird.describe, which runs because Duck did not override it. From it, the downward ink arrow is the call self.sound(). The two candidate sound methods are stacked below: Bird.sound = tweet (ink) and Duck.sound = quack (orange). The orange arrow from self to Duck.sound is the point of the figure: because self is a Duck, the call lands on the orange one, not the ink one — even though we launched from inside Bird.
Steps
Duck() has no describe; MRO (Duck, Bird, object) finds Bird.describe. It runs.
Why this step? Cell-2 logic — inherited method found in Bird.
Inside, self.sound() is evaluated. self is still a Duck.Why this step?self carries the object's type; being physically inside Bird's code does not change that.
So self.sound() re-runs the lookup on Duck.__mro__ → Duck.sound → "quack".
Why this step? This is polymorphism: an inherited method calls the child's override.
Recall Answer
"A bird that says quack".
Verify:type(self) is Duck, so sound resolves to Duck.sound → "quack". ✓
How to read Figure s05. The same MRO row: TimeLogger (orange), Logger (teal), object (ink). The first orange arrow shows the normal lookup landing on TimeLogger.log. The second, plum arrow labelled super() skips pastTimeLogger and lands on Logger.log — showing that super() starts one step later, so it can never re-enter the current class (no infinite loop). The two returned pieces are joined at the bottom.
Steps
MRO (TimeLogger, Logger, object). TimeLogger.log runs first.
Why this step? Direct hit in the child.
super() inside TimeLogger = "restart the walk at the class afterTimeLogger" = Logger.
Why this step?super() is next-in-MRO, so it cannot re-enter the current class — no infinite loop.
Logger.log() → "[LOG]"; then we concatenate " 12:00".
Why this step? Extend = parent's result plus our addition. See super().
How to read Figure s06. The boxes are the four classes plus object, laid out in MRO order left-to-right: D and C (orange, "none" = they don't define kind), then B (teal, kind=B), then A (plum, kind=A), then object (ink). The ink arrows are the walk stepping rightward. The bold orange rectangle around B marks where the search stops — the first box with kind in it — and the "STOP: first match" label points there. A also has kind, but the walk never reaches it, which is the whole lesson: the nearer ancestor shadows the farther one.
Steps
MRO is the whole chain: (D, C, B, A, object) (this is exactly what D.__mro__ prints).
Why this step? Single inheritance stacks parents most-specific first.
Walk: D no, C no, Byes → stop at B.
Why this step? First match wins; we never reach A even though it also has kind.
Result "B" — the nearest ancestor that defines the name shadows the farther one.
Why this step? "Most specific override closest to the object beats a more general one."
Recall Answer
MRO (D, C, B, A, object); call returns "B".
Verify: first class along (D,C,B,A,object) with kind defined is B. ✓
How to read Figure s07. Same walk, but every box — Kid (orange), Base (teal), object (ink) — is labelled "none". The ink arrows step through all three and then off the right edge of the row into a red-ringed "AttributeError" box. There is no orange stop ring anywhere, because nothing ever matched: this is what "falling off the end of the MRO" looks like.
Steps
MRO (Kid, Base, object). Search fly in each __dict__.
Why this step? Same algorithm, no shortcuts for missing names.
Not in Kid, not in Base, not in object.
Why this step?object is the final stop — the wall at the end of the walk.
Walk falls off the end → Python raises AttributeError: 'Kid' object has no attribute 'fly'.
Why this step? "First match" with no match is the only failure mode.
Recall Answer
Raises AttributeError — the well-defined "not found anywhere" outcome.
Verify:hasattr(Kid(), "fly") is False, confirming the walk finds nothing. ✓
How to read Figure s08. This figure emphasises the first step of the mental model (s01): the instance check. The plum box at top is t.__dict__, now holding tick = 5. The purple arrow shows the lookup finding tickright there and stopping — so the teal Timer.tick method box below (holding the real method) is greyed out and never reached. The bottom label shows that calling the found value, an int, raises TypeError.
Steps
Lookup checks the instance __dict__ first, before the class MRO.
Why this step? The instance-first rule from Encapsulation and Attribute Lookup: personal storage beats the class.
t.__dict__ now contains tick = 5, so t.tick returns the integer 5 — the method is completely hidden.
Why this step? A data attribute and a method share the same nametick; whoever is found first wins, and the instance is found first.
Line B does t.tick() = (5)() = trying to call an integer. Integers are not callable → TypeError: 'int' object is not callable.
Why this step? This is the sharp end of attribute-vs-method masking: the name still exists, but it no longer points at something you can call.
Recall Answer
Line A prints 5 (the data attribute masks the method); line B raises TypeError: 'int' object is not callable.
Verify:t.tick is the int5; callable(5) is False, so calling it fails. ✓
How to read Figure s09. The MRO row for the drink hierarchy: Mocha (orange), Latte (teal), Drink (plum), object (ink). The plum super() arrows chain rightward one step at a time — Mocha.price calls Latte.price calls Drink.price. The running totals are stacked underneath each box: Drink returns 100, Latte adds 40 → 140, Mocha adds 30 → 170. The figure shows how super() climbs the chain and the additions stack on the way back down.
Steps
is-a chain: Mocha is-a Latte is-a Drink → single inheritance, three levels.
Why this step? "is-a" (not "has-a") tells us to inherit, per the parent note's 80/20 rule.
Each level uses super().price() and adds its own cost:
How to read Figure s10. The plum box is s.__dict__after construction: it holds only roll = 7 — the name slot is drawn empty/crossed-out because Student.__init__ never called super().__init__. The lower row shows the lookup for s.name: it misses the instance dict, then walks Student → Person → object (all labelled "no data attr name") and falls off the end into the red AttributeError box. The contrast with the filled roll slot is the lesson: only the field you actually assigned exists.
Steps
Defining __init__ in StudentreplacesPerson.__init__ entirely.
Why this step?__init__ is just a method; the override rule (Cell 5) applies — the child's version shadows the parent's.
Student.__init__ sets self.roll but never calls super().__init__, so self.name is never assigned.
Why this step? Unlike other methods, __init__ is not auto-chained; you must call the parent yourself.
s.roll → 7. Then s.name searches instance dict (no name), then MRO (name is not a class attribute either) → AttributeError.
Why this step? Same lookup as WE-6/WE-7 — the field was simply never created.
Recall Answer
s.roll prints 7; s.name raises AttributeError because super().__init__ was skipped.
Verify:hasattr(s, "roll") True, hasattr(s, "name") False (name never set). ✓
All nine cells are really one flowchart. Read it as the life of a single lookup obj.name: first Python checks the object's own storage (instance dict); if the name isn't there it walks the class MRO left-to-right; the first class that has the name ends the search; and if the walk runs past object with no match, you get an AttributeError. Trace any example above through these exact arrows and you will land on its answer.
describe calls self.sound() and self is a Duck. Which sound?
Duck.sound — dispatch uses type(self), not where the code text lives. Result "quack".
Recall WE-5: in
(D, C, B, A, object) where both A and B define kind, which wins?
B — the nearest (most specific) ancestor along the MRO shadows the farther one.
Recall WE-7: instance stores
t.tick = 5 while the class defines a method tick. What do t.tick and t.tick() do?
t.tick returns 5 (the data attribute masks the method); t.tick() raises TypeError: 'int' object is not callable.
Recall WE-9: child defines
__init__ but omits super().__init__. Fate of the parent's field?
Never created → accessing it raises AttributeError. __init__ is not auto-chained.