2.1.9 · D3OOP Fundamentals

Worked examples — `super()` — calling parent methods

2,818 words13 min readBack to topic

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.

Figure — `super()` — calling parent methods

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 surprisesuper() 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 Toyota
  1. super().__init__()Why this step? It runs Vehicle.__init__, which creates self.wheels. We call it first so the parent's setup exists before we add ours. No self is passed: the proxy already carries it.
  2. self.brand = brandWhy 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 False
  1. super().check(n)Why this step? We reuse the parent's positivity rule instead of copying n > 0. If Validator changes its rule later, the child follows automatically (single source of truth).
  2. 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 in base — a normal method hands us data, unlike __init__ which returns nothing.

Verify:

  • n=4: 4>0 is True, 4%2==0 is TrueTrue. ✓
  • n=3: positive but 3%2==1False. ✓
  • n=-2: -2>0 is False → short-circuits to False. ✓

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
  1. Read the MROWhy 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).
  2. super().__init__()Why this step? The next person after Base is object, 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'
  1. Construction succeedsWhy this step? Nothing forces the parent's __init__ to run; Python happily builds the object with only rate. The bug is silent here.
  2. Reading s.balance failsWhy this step? Account.__init__ never ran, so balance was never assigned. The AttributeError appears 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.

Figure — `super()` — calling parent methods
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']
  1. Build the MRO lineWhy 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.
  2. Follow super() down the lineWhy this step? In B, super() does not mean "go to A" — it means "go to the next person after B in this line," which is C, not A. So A is reached only once, after C.
  3. Count AWhy this step? Because the line lists A exactly once, its __init__ runs exactly once. This is the property you cannot get by writing A.__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']
  1. super().greet() from DWhy this step? Next in line after D is B. B has no greet, so Python keeps walking the same line: next is C, which does define greet.
  2. Lands in C, not AWhy this step? Even though C is a sibling of B (not an ancestor of D in the tree-picture), it comes before A in 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']
  1. Each class grabs its keyword, keeps the rest in **kwWhy this step? No class knows the whole set of arguments; each peels off its own (name or color) and forwards the remainder up the MRO line.
  2. super().__init__(**kw)Why this step? By the time the leftover kwargs reach Base, they're empty, so Base.__init__() (which takes nothing) succeeds. If any class forgot to forward, an argument would get "stuck" and cause a TypeError.

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 2
  1. super().__init__()Why this step? Ensures self.balance exists (from Account) before we add self.log. Skipping it is the C4 trap.
  2. super().deposit(amount)Why this step? Reuse the tested balance logic; we don't re-implement balance += amount and risk a bug.
  3. 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)
  1. Part (a) traces the single MRO lineWhy? The MRO is D → B → C → A → object, each visited once, giving D B C A.
  2. Part (b) breaks the chainWhy the double-call? Calling B.__init__(self) triggers B's own super(), which — because self is a D2 — continues down D2's MRO into C then A. So the first manual call already ran B C A. Then the second manual line C.__init__(self) runs C again, whose super() runs A again. The "explicit" approach ran A twice and C twice — the exact bug super() prevents.

Verify: Part (a) prints D B C A. Part (b) prints D2 B C A C AA 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

Concept Map

straight line

tail is object

skip setup

flattened diamond

walks past B

forward leftovers

reuse then add

bypassed by

super() = next in MRO line

C1 single inheritance

C3 reaches object at the tail

C4 forgetting super breaks later

C5 diamond base runs once

C6 lands on sibling

C7 cooperative kwargs

C8 audited account extends

C9 manual call double-runs base