Worked examples — `super()` — calling parent methods
Before any code, one word we lean on constantly: MRO = Method Resolution Order = the single flat list Python builds of which class to look in, and in what order, when you ask an object for a method. Picture it as a queue of people standing in a line; super() just means "hand the job to the next person in line after me." We visualise that line in the first figure.

Look at the red box in the figure: that is you (the current class). super() does not point at your parent by name — it points at whoever stands immediately to your right in the MRO line. In a simple family that happens to be the parent; in a diamond it can be a sibling. Keep that picture in mind for every example below.
The scenario matrix
Every situation this topic can throw is one of these cells:
| # | Cell (the case) | Hit by |
|---|---|---|
| C1 | Single inheritance, child extends __init__ |
Example 1 |
| C2 | Extending a normal (non-__init__) method, using the return value |
Example 2 |
| C3 | Degenerate: no explicit parent (super() in a top-level class → object) |
Example 3 |
| C4 | "Zero" case: forgetting super() — what actually breaks |
Example 4 |
| C5 | Diamond inheritance — base runs exactly once | Example 5 |
| C6 | Sibling surprise — super() in a child jumps sideways, not up |
Example 6 |
| C7 | Cooperative keyword arguments — passing args through a chain safely | Example 7 |
| C8 | Real-world word problem — bank account + audited account | Example 8 |
| C9 | Exam twist — predict the print order & spot the double-call bug | Example 9 |
The "signs/quadrants" of this topic are the shapes of the inheritance graph (straight line vs diamond) and the degenerate inputs (no parent, no super-call, empty args). We cover every one.
Example 1 — C1: single inheritance, extend __init__
Forecast: guess before reading — after Car("Toyota"), how many attributes does the object have, and which line created each?
class Vehicle:
def __init__(self):
self.wheels = 4
class Car(Vehicle):
def __init__(self, brand):
super().__init__() # step 1
self.brand = brand # step 2
c = Car("Toyota")
print(c.wheels, c.brand) # 4 Toyotasuper().__init__()— Why this step? It runsVehicle.__init__, which createsself.wheels. We call it first so the parent's setup exists before we add ours. Noselfis passed: the proxy already carries it.self.brand = brand— Why this step? The child adds new state on top of the inherited state. Overriding means extend, not erase.
Verify: c.wheels == 4 (from the parent line) and c.brand == "Toyota" (from the child line). Two attributes, one from each class — exactly what the MRO line Car → Vehicle → object predicts.
Example 2 — C2: extend a normal method and use its return
Forecast: for n = 4, n = 3, n = -2 — what should each return?
class Validator:
def check(self, n):
return n > 0
class PositiveEven(Validator):
def check(self, n):
base = super().check(n) # step 1: reuse parent's rule
return base and (n % 2 == 0) # step 2: add our rule
v = PositiveEven()
print(v.check(4), v.check(3), v.check(-2)) # True False Falsesuper().check(n)— Why this step? We reuse the parent's positivity rule instead of copyingn > 0. IfValidatorchanges its rule later, the child follows automatically (single source of truth).base and (n % 2 == 0)— Why this step? We combine the inherited answer with the new "even" requirement. This is why we captured the return value inbase— a normal method hands us data, unlike__init__which returns nothing.
Verify:
n=4:4>0isTrue,4%2==0isTrue→True. ✓n=3: positive but3%2==1→False. ✓n=-2:-2>0isFalse→ short-circuits toFalse. ✓
Example 3 — C3: the degenerate top — super() reaches object
Forecast: every class in Python secretly inherits from one thing. What is it, and what does its __init__ do?
class Base:
def __init__(self):
print("Base init")
super().__init__() # goes to object.__init__
Base.__mro__ # (Base, object)
Base() # prints: Base init- Read the MRO — Why this step?
Base.__mro__is(Base, object). Even with no parent written, ==object== is appended automatically — it is the last person in every MRO line (see the tail of the figure). super().__init__()— Why this step? The next person afterBaseisobject, whose__init__does nothing but exists. So the call is harmless and finishes cleanly. This is the degenerate / limiting case: the chain still has a valid "next", it just happens to do nothing.
Verify: Base.__mro__ == (Base, object) and the last element is object. The call succeeds because object.__init__ accepts a bound self with no extra args.
Example 4 — C4: the "zero" case — forgetting super().__init__()
Forecast: the crash won't happen where the mistake is. Guess: at construction, or later when an inherited attribute is read?
class Account:
def __init__(self):
self.balance = 0
class Savings(Account):
def __init__(self):
self.rate = 0.05 # forgot super().__init__()
s = Savings()
print(s.rate) # 0.05 — works fine!
print(s.balance) # AttributeError: 'Savings' object has no attribute 'balance'- Construction succeeds — Why this step? Nothing forces the parent's
__init__to run; Python happily builds the object with onlyrate. The bug is silent here. - Reading
s.balancefails — Why this step?Account.__init__never ran, sobalancewas never assigned. TheAttributeErrorappears far from the real cause — this is why the mistake is dangerous.
Verify: Savings() builds without error, s.rate == 0.05, and accessing s.balance raises AttributeError (checked in VERIFY by confirming balance is not in the instance dict).
Example 5 — C5: the diamond — base runs exactly once
Forecast: naive guess is D B A C A (A twice). Write down your guess, then compare.

class A:
def __init__(self): print("A"); super().__init__()
class B(A):
def __init__(self): print("B"); super().__init__()
class C(A):
def __init__(self): print("C"); super().__init__()
class D(B, C):
def __init__(self): print("D"); super().__init__()
D() # prints: D B C A
print([k.__name__ for k in D.__mro__]) # ['D','B','C','A','object']- Build the MRO line — Why this step? Python's C3 linearization (see Method Resolution Order (MRO) & C3 Linearization) flattens the diamond into one straight line:
D → B → C → A → object. The figure shows the diamond collapsed into this line. - Follow
super()down the line — Why this step? InB,super()does not mean "go toA" — it means "go to the next person afterBin this line," which isC, notA. SoAis reached only once, afterC. - Count
A— Why this step? Because the line listsAexactly once, its__init__runs exactly once. This is the property you cannot get by writingA.__init__(self)by hand.
Verify: D.__mro__ names are ['D','B','C','A','object']; the print order is D B C A; A appears exactly once. All three checked in VERIFY.
Example 6 — C6: sibling surprise — super() jumps sideways
Forecast: if super() meant "my parent," it would land in B. Does it?
class A:
def greet(self): return "A-greet"
class B(A):
pass
class C(A):
def greet(self): return "C-greet"
class D(B, C):
def greet(self): return "D->" + super().greet()
print(D().greet()) # D->C-greet
print([k.__name__ for k in D.__mro__]) # ['D','B','C','A','object']super().greet()fromD— Why this step? Next in line afterDisB.Bhas nogreet, so Python keeps walking the same line: next isC, which does definegreet.- Lands in
C, notA— Why this step? Even thoughCis a sibling ofB(not an ancestor ofDin the tree-picture), it comes beforeAin the flat MRO line. This is the headline lesson:super()follows the line, not the family tree.
Verify: D().greet() == "D->C-greet". Confirms super() skipped B (no method) and stopped at the sibling C before reaching A.
Example 7 — C7: cooperative keyword arguments through a chain
Forecast: each __init__ must accept the leftovers and forward them. What does each class pass to super().__init__(...)?
class Base:
def __init__(self):
pass
class Colored(Base):
def __init__(self, color=None, **kw): # keep leftovers in kw
self.color = color
super().__init__(**kw) # forward the rest
class Named(Base):
def __init__(self, name=None, **kw):
self.name = name
super().__init__(**kw)
class Widget(Named, Colored):
pass
w = Widget(name="X", color="red")
print(w.name, w.color) # X red
print([k.__name__ for k in Widget.__mro__])
# ['Widget', 'Named', 'Colored', 'Base', 'object']- Each class grabs its keyword, keeps the rest in
**kw— Why this step? No class knows the whole set of arguments; each peels off its own (nameorcolor) and forwards the remainder up the MRO line. super().__init__(**kw)— Why this step? By the time the leftover kwargs reachBase, they're empty, soBase.__init__()(which takes nothing) succeeds. If any class forgot to forward, an argument would get "stuck" and cause aTypeError.
Verify: w.name == "X" and w.color == "red"; MRO is Widget → Named → Colored → Base → object, so both mixins run once and both attributes get set.
Example 8 — C8: real-world word problem
Forecast: balance and log length after two deposits — guess both numbers.
class Account:
def __init__(self):
self.balance = 0
def deposit(self, amount):
self.balance += amount
return self.balance
class AuditedAccount(Account):
def __init__(self):
super().__init__() # step 1
self.log = []
def deposit(self, amount):
new_balance = super().deposit(amount) # step 2
self.log.append(amount) # step 3
return new_balance
a = AuditedAccount()
a.deposit(100)
a.deposit(50)
print(a.balance, len(a.log)) # 150 2super().__init__()— Why this step? Ensuresself.balanceexists (fromAccount) before we addself.log. Skipping it is the C4 trap.super().deposit(amount)— Why this step? Reuse the tested balance logic; we don't re-implementbalance += amountand risk a bug.self.log.append(amount)— Why this step? The child's added responsibility: an audit trail on top of the inherited behaviour.
Verify: after depositing 100 then 50, balance = 0 + 100 + 50 = 150 and log = [100, 50] so len(log) = 2. Units: money in, money accumulated; count of entries matches count of deposits. ✓
Example 9 — C9: exam twist — predict output & spot the double-call bug
Forecast: write down part (a)'s output before reading on; for part (b), guess how many times A prints.
# Part (a) — correct, cooperative super()
class A:
def __init__(self): print("A"); super().__init__()
class B(A):
def __init__(self): print("B"); super().__init__()
class C(A):
def __init__(self): print("C"); super().__init__()
class D(B, C):
def __init__(self): print("D"); super().__init__()
D() # prints: D B C A (A once)
# Part (b) — the "explicit" bug
class D2(B, C):
def __init__(self):
print("D2")
B.__init__(self) # runs B -> its super() -> C -> its super() -> A
C.__init__(self) # runs C -> its super() -> A AGAIN
D2() # prints: D2 B C A C A (A twice, C twice)- Part (a) traces the single MRO line — Why? The MRO is
D → B → C → A → object, each visited once, givingD B C A. - Part (b) breaks the chain — Why the double-call? Calling
B.__init__(self)triggersB's ownsuper(), which — becauseselfis aD2— continues downD2's MRO intoCthenA. So the first manual call already ranB C A. Then the second manual lineC.__init__(self)runsCagain, whosesuper()runsAagain. The "explicit" approach ranAtwice andCtwice — the exact bugsuper()prevents.
Verify: Part (a) prints D B C A. Part (b) prints D2 B C A C A — A appears twice, C appears twice. (Checked in VERIFY by capturing the print sequences.)
Recall Rapid recall — one line per cell
Single inheritance: super().__init__() runs the parent, no self. ::: C1 ✓
Extending a method: capture super().method()'s return and build on it. ::: C2 ✓
Top-level class: super() still valid — it reaches harmless object. ::: C3 ✓
Skipping super in __init__: silent build, later AttributeError. ::: C4 ✓
Diamond D(B,C): MRO D B C A object, base runs once. ::: C5 ✓
super() can land on a sibling, not an ancestor. ::: C6 ✓
Cooperative kwargs: each class peels its arg, forwards **kw. ::: C7 ✓
Manual Parent.__init__(self) in a diamond → double-runs the base. ::: C9(b) ✓
Connections
- ← Back to the parent topic
- Inheritance — extending classes
- Method Resolution Order (MRO) & C3 Linearization
- Method Overriding vs Overloading
- Multiple Inheritance & the Diamond Problem
- `__init__` — constructors
- Composition over Inheritance