Exercises — Inheritance — single inheritance, method resolution order (MRO)
Before we start, one shared vocabulary reminder so no symbol is unearned:

Level 1 — Recognition
Goal: read a hierarchy and state its MRO / lookup result mechanically.
Exercise 1.1
class A: pass
class B(A): pass
class C(B): passWrite C.__mro__ as a tuple.
Recall Solution 1.1
> Single inheritance is just the parent chain, ending in the universal root `object`.
Walk up: C → its parent B → its parent A → A's implicit parent object.
Why object at the end? Every class you write silently inherits from object, so it is always the last, most-general stop.
Exercise 1.2
class Animal:
def speak(self): return "..."
class Dog(Animal): pass
print(Dog().speak())What prints, and which class's speak ran?
Recall Solution 1.2
MRO of Dog = (Dog, Animal, object). Search for speak:
Dogbody → nospeak. Skip.Animalbody → hasspeak. ✅ Stop.
Output: ... — the method that ran lives in Animal (inherited, not overridden).
Level 2 — Application
Goal: apply override and super() to produce concrete outputs.
Exercise 2.1
class A:
def who(self): return "A"
class B(A):
def who(self): return "B"
class C(B): pass
print(C().who())What prints?
Recall Solution 2.1
MRO = (C, B, A, object). Search who:
C→ none. Skip.B→ defineswho. ✅ Stop.
Output: B. A.who exists too, but we never reach it — the first match wins.
Exercise 2.2
class Base:
def greet(self): return "hi"
class Child(Base):
def greet(self): return super().greet() + " there"
print(Child().greet())What prints, and where does super() resume the search?
Recall Solution 2.2
Child.greet runs first (most specific). Inside it, super() means "continue the same MRO search, starting after Child."
MRO = (Child, Base, object). Position after Child is Base → Base.greet() returns "hi".
Then we append " there".
Output: hi there.
Exercise 2.3
class Account:
def __init__(self, balance):
self.balance = balance
class Savings(Account):
def __init__(self, balance, rate):
super().__init__(balance)
self.rate = rate
s = Savings(200, 0.03)
print(s.balance, s.rate)What prints?
Recall Solution 2.3
Savings.__init__ runs. Line by line:
super().__init__(balance)=Account.__init__(self, 200)→ setsself.balance = 200.self.rate = 0.03.
Output: 200 0.03. The super() call is what makes balance exist — see super() and cooperative multiple inheritance.
Level 3 — Analysis
Goal: trace dispatch through type(self), not "where the code lives".
Exercise 3.1
class X:
def m(self): return "X." + self.tag()
def tag(self): return "x"
class Y(X):
def tag(self): return "y"
print(Y().m())Predict the output and justify which tag runs.
Recall Solution 3.1
The object is a Y, so type(self) is Y and its MRO is (Y, X, object).
Y().m():Yhas nom,Xdoes → runX.m.- Inside
X.m,self.tag()is a fresh lookup againsttype(self).__mro__ = (Y, X, object). Ydefinestag→ runY.tag→"y".
Output: X.y. This is polymorphism: inherited code (X.m) calls the overridden method.
Exercise 3.2
class A:
def f(self): return "A.f"
class B(A):
def f(self): return "B.f->" + super().f()
class C(B):
def f(self): return "C.f->" + super().f()
print(C().f())Trace every super() hop.
Recall Solution 3.2
MRO = (C, B, A, object).
C.fruns →"C.f->"+super().f().super()afterC→B.f.B.fruns →"B.f->"+super().f().super()afterB→A.f.A.fruns →"A.f".
Unwinding: A.f = "A.f"; B.f = "B.f->A.f"; C.f = "C.f->B.f->A.f".
Output: C.f->B.f->A.f.
Level 4 — Synthesis
Goal: combine override + super() + type(self) and build correct behaviour.
Exercise 4.1
Given:
class Shape:
def describe(self): return f"a {self.name()}"
def name(self): return "shape"
class Circle(Shape):
def name(self): return "circle"
class Ring(Circle):
def describe(self): return "hollow " + super().describe()
def name(self): return "ring"
print(Ring().describe())Predict output. Then answer: which name() does the inherited Shape.describe use?
Recall Solution 4.1
MRO = (Ring, Circle, Shape, object).
Ring.describeruns →"hollow "+super().describe().super()afterRing→Circlehas nodescribe→ keep going in MRO →Shape.describe.Shape.describe=f"a {self.name()}". Hereselfis still aRing! Soself.name()searches(Ring, Circle, Shape, object)→Ring.name→"ring".- So
Shape.describereturns"a ring".
Final: "hollow " + "a ring" = hollow a ring.
Key insight: Shape.describe uses Ring.name, because self's dynamic type drives the second lookup.
Exercise 4.2
You are given a Stack that (wrongly) inherits from list to reuse append/pop:
class Stack(list):
def push(self, x): self.append(x)Explain one concrete way this breaks the "stack" promise, and rewrite it with composition.
Recall Solution 4.2
The break: because Stack is-a list, it also exposes insert, __getitem__, etc. A caller can do s.insert(0, 99) and jump the queue — a stack is only supposed to touch the top. The is-a contract leaked a bigger interface than intended.
Composition fix (Composition over Inheritance) — has-a a list instead of is-a a list:
class Stack:
def __init__(self):
self._items = [] # has-a list
def push(self, x): self._items.append(x)
def pop(self): return self._items.pop()
def __len__(self): return len(self._items)Now only push/pop/len are public — the underlying list is hidden (encapsulated). No insert leak.
Rule: inherit only for genuine is-a; wrap for has-a.
Level 5 — Mastery
Goal: design/reason about a whole hierarchy and predict subtle interactions.
Exercise 5.1
Consider:
class Logger:
def log(self, msg): return f"[{self.level()}] {msg}"
def level(self): return "INFO"
class WarnLogger(Logger):
def level(self): return "WARN"
class TimedWarn(WarnLogger):
def log(self, msg): return "t=0 " + super().log(msg)
print(TimedWarn().log("disk full"))Give the output and the full MRO, then explain why the inner self.level() picks WARN.
Recall Solution 5.1
MRO = (TimedWarn, WarnLogger, Logger, object).
TimedWarn.logruns →"t=0 "+super().log("disk full"). AfterTimedWarn→WarnLoggerhas nolog→Logger.log.Logger.log=f"[{self.level()}] {msg}".selfis aTimedWarn;self.level()searches the MRO →TimedWarnhas nolevel,WarnLoggerdoes →"WARN".- So
Logger.logreturns"[WARN] disk full".
Final: "t=0 [WARN] disk full".
Why WARN not INFO? Even though the string self.level() sits inside Logger, dispatch uses type(self).__mro__, and the first level found there is WarnLogger.level.
Exercise 5.2
Design task. You must model Vehicle → Car → ElectricCar where:
Vehicle.__init__(self, wheels)setsself.wheels.Car.__init__(self)should give 4 wheels.ElectricCar.__init__(self, battery)should keep 4 wheels and setself.battery.
Write the three classes so that ElectricCar(75).wheels == 4 and .battery == 75, using super() correctly. Then state each object's MRO.
Recall Solution 5.2
class Vehicle:
def __init__(self, wheels):
self.wheels = wheels
class Car(Vehicle):
def __init__(self):
super().__init__(4) # Vehicle sets wheels = 4
class ElectricCar(Car):
def __init__(self, battery):
super().__init__() # Car.__init__ -> sets wheels = 4
self.battery = batteryTrace for ElectricCar(75) — MRO = (ElectricCar, Car, Vehicle, object):
ElectricCar.__init__(75)→super().__init__()=Car.__init__(self).Car.__init__→super().__init__(4)=Vehicle.__init__(self, 4)→self.wheels = 4.- back in
ElectricCar:self.battery = 75.
Result: wheels == 4, battery == 75. ✅
MROs:
Car.__mro__ = (Car, Vehicle, object)ElectricCar.__mro__ = (ElectricCar, Car, Vehicle, object)
Exercise 5.3 (degenerate/edge case)
class Empty: pass
e = Empty()
print(Empty.__mro__)
print(e.__str__()) # does this error?What is the MRO of a class with no explicit parent, and where does __str__ come from?
Recall Solution 5.3
A class with no written parent still inherits from object. So:
e.__str__() does not error: Empty has no __str__, but the search continues to object, which defines a default __str__ (returning something like '<__main__.Empty object at 0x...>'). This is the base case of inheritance — even "no parent" means "parent = object". See object — the root class.
Quick self-test wrap-up
Term reveals (cover the right side):
Which class in an MRO wins a lookup?
super() resumes the search at which position?
type(self)'s MRO.Inside inherited code, self.foo() resolves against?
type(self).__mro__ — the object's dynamic type, not where the code lives.MRO of a parentless class class Q: pass?
(Q, object).