2.1.8 · D5OOP Fundamentals

Question bank — Inheritance — single inheritance, method resolution order (MRO)

1,762 words8 min readBack to topic

Figure 1 — the single-inheritance MRO as a straight ladder the lookup climbs:

Figure — Inheritance — single inheritance, method resolution order (MRO)

Figure 2 — a diamond (multiple inheritance): the trap the single-chain intuition gets wrong:

Figure — Inheritance — single inheritance, method resolution order (MRO)

True or false — justify

TF1. In single inheritance, object always appears exactly once, at the very end of the Method Resolution Order.
True — every class ultimately derives from object, and single inheritance is a straight chain (Figure 1), so it can only be reached one way and sits last.
TF2. Overriding a method in a child deletes it from the parent.
False — the parent's method still exists and still runs for parent instances; the child's version only shadows it during lookup on child instances.
TF3. If a child defines __init__, the parent's __init__ runs automatically before it.
False — defining __init__ in the child replaces the parent's; it only runs if you explicitly call super().__init__(...).
TF4. super() always means "my direct parent class."
False — super() means "the next class in the MRO after the current one." For single inheritance that happens to equal the parent, but in the diamond of Figure 2 the next class after B is C, not B's own parent A.
TF5. An inherited method always uses methods from the class where its code text lives.
False — calls like self.tag() inside it dispatch through type(self).__mro__, so an overridden version in the actual object's class wins. That is polymorphism.
TF6. Cls.mro() and Cls.__mro__ give the same ordering.
True — mro() is the method that computes it and __mro__ is the cached tuple result; both list classes most-specific to most-general.
TF7. A child instance can access attributes set only in the parent's __init__, even if the child never mentions them.
True — provided the parent's __init__ actually ran (directly or via super()), it set those attributes on the same self, which is one shared object.
TF8. Making Stack inherit from list is a clean way to reuse append.
False — it also inherits insert, pop(0), indexing, etc., breaking the "stack" contract; this is an is-a violation better solved by composition.
TF9. In the diamond class D(B, C) where B, C both inherit A, the MRO visits A right after B.
False — the MRO is D, B, C, A, object; A is deferred until after C because both B and C derive from it (the C3 rule of Figure 2).

Spot the error

SE1. "class Dog(Animal) copies Animal's code into Dog at definition time."
Wrong — nothing is copied; Dog just holds a reference to Animal, and lookup walks up to Animal at call time. Fix the parent and every child sees the fix.
SE2. "super().__init__(self, balance) — pass self explicitly like a normal call."
Wrong — super() already binds the current instance, so you write super().__init__(balance) with no self. Passing self gives it too many arguments.
SE3. "To skip a broken parent method I'll remove it from the MRO."
Wrong — you cannot edit the MRO; instead override the method in the child so the child's version is found first during lookup.
SE4. "Inside X.m, self.tag() calls X.tag because we're in class X."
Wrong — self.tag() starts lookup at type(self), so if self is a Y with its own tag, Y.tag runs even though the code sits in X.
SE5. "c.who() searches Dog, then object, skipping the middle classes."
Wrong — lookup visits every class in C.__mro__ in order (C, B, A, object) and stops at the first that defines the name; no class is skipped (the red pointer in Figure 1 steps through each rung).
SE6. "super().describe() returns the child's describe since super points back to me."
Wrong — super() starts after the current class, so it deliberately skips the child's own describe and finds the next class's version.
SE7. "In class D(B, C), calling super().greet() in B must go to A since A is B's parent."
Wrong — from a D instance, super() inside B goes to C (the next class in D's MRO), not to A. This cooperative hand-off is the whole point of super().

Why questions

WY1. Why does Python precompute the MRO instead of searching live each time?
To make lookup deterministic and fast — one fixed order gives the same answer every call and lets Python cache it as a tuple.
WY2. Why is object the root of every class?
It provides the default machinery (__init__, __str__, __eq__, etc.), so every object has a fallback behavior and lookup always terminates instead of running off the end. See object — the root class.
WY3. Why do we say "is-a → inherit, has-a → compose"?
Inheritance imposes a substitutability contract (a child must be usable wherever the parent is), which only makes sense for a genuine is-a relationship; a has-a is just a stored field.
WY4. Why does self keep the object's real type inside inherited methods?
Because there is only one object, and dispatch is bound to that object's dynamic type — this is what lets a general parent method call a specialized child method automatically.
WY5. Why is super() preferred over naming the parent class directly?
super() follows the MRO, so it keeps working under refactoring and (with cooperative multiple inheritance) chains correctly through the diamond of Figure 2 where a hard-coded name would visit A too early.
WY6. Why doesn't overriding break code that called the parent method?
Because callers use type(self) dispatch, they transparently get the most-specific version; the override participates in the same interface, which is the point of polymorphism.
WY7. Why can't the diamond MRO just be D, B, A, C, A, object?
A class may appear only once in an MRO, and a class must come before all its ancestors; C3 enforces both, producing D, B, C, A, object.

Edge cases

EC1. What is the MRO of a class with no explicit parent, like class A:?
(A, object) — an implicit parent of object is always inserted, so even a "base" class has a two-element MRO.
EC2. What happens if no class in the MRO defines the looked-up name?
Python raises AttributeError after exhausting the whole MRO including object — lookup never fails silently.
EC3. If a child overrides only speak but not __init__, which __init__ runs on the child?
The parent's __init__, found by walking the MRO — the child inherits it unchanged because it defines no __init__ of its own.
EC4. Calling super().method() from the last user-defined class before object — what runs?
Lookup continues into object; if object defines that method (like __str__) it runs, otherwise you get AttributeError.
EC5. Two classes in the chain both define who; a grandchild instance calls who. Which runs?
The one from whichever class appears earlier (more specific) in the MRO — the first match stops the search, the later definition is shadowed.
EC6. A parent method reads self.rate, but only the child sets it. Is that valid?
Only if the child's __init__ actually ran and set self.rate before the method is called; otherwise you hit AttributeError because the attribute was never created on this object.
EC7. Does redefining a method in the child change the MRO itself?
No — the MRO is a list of classes, fixed by the inheritance structure; overriding changes which class wins a lookup, not the ordering of classes.
EC8. What if a diamond's inheritance is inconsistent, e.g. class D(A, B) where B inherits A?
Python raises a TypeError at class-creation time — C3 cannot produce a valid order that keeps every class before its ancestors, so the class simply won't build.

Active Recall

Recall One sentence: what does

super() actually point to? The next class after the current one in type(self).__mro__, which in a diamond is a sibling branch, not necessarily the literal parent.

Recall One sentence: what determines which method runs on

self.foo()? The dynamic type of the object, type(self), searched through its MRO — regardless of where the calling code physically lives.

Recall One sentence: what is the MRO of

class D(B, C) with B, C both from A? D, B, C, A, objectA is pushed to just before object by C3 linearization.