2.1.9 · D4OOP Fundamentals

Exercises — `super()` — calling parent methods

2,052 words9 min readBack to topic

The core rule we lean on everywhere below, in plain words:

The picture that makes MRO tracing physical:

Figure — `super()` — calling parent methods

Level 1 — Recognition

Exercise 1.1

Which of these calls is written correctly inside Dog.__init__, given Animal.__init__(self, name)?

# (a) super().__init__(self, name)
# (b) super().__init__(name)
# (c) super.__init__(name)
# (d) Animal.__init__(name)
Recall Solution 1.1

Answer: (b).

  • (a) passes self twice — the proxy already bound it → TypeError: got multiple values for argument 'name' (or for self).
  • (c) super without () is the class object super, not a proxy — super.__init__ is meaningless here.
  • (d) Animal.__init__(name) calls the raw function with name sitting in the self slot — so self becomes the string "Rex". Wrong and dangerous.
  • (b) passes only the remaining argument. Correct.

Exercise 1.2

For class Dog(Animal): pass with class Animal: pass, what is Dog.__mro__?

Recall Solution 1.2

(Dog, Animal, object). Every class ultimately inherits from object, so it is always the last entry.


Level 2 — Application

Exercise 2.1

What does this print?

class Vehicle:
    def __init__(self, wheels):
        self.wheels = wheels
 
class Car(Vehicle):
    def __init__(self, brand):
        super().__init__(4)
        self.brand = brand
 
c = Car("Toyota")
print(c.wheels, c.brand)
Recall Solution 2.1

Prints 4 Toyota.

  • Car("Toyota") calls Car.__init__ with brand="Toyota".
  • super().__init__(4) steps to the next class after Car in (Car, Vehicle, object)Vehicle, and runs Vehicle.__init__(self, 4), so self.wheels = 4.
  • Then self.brand = "Toyota".

Exercise 2.2

Extend a method (not just __init__). What is the return value and printed output?

class Base:
    def price(self):
        return 100
 
class Taxed(Base):
    def price(self):
        base = super().price()
        return base * 1.1   # add 10% tax
 
print(Taxed().price())
Recall Solution 2.2

Prints 110.0.

  • super().price() runs Base.price(self) → returns 100.
  • 100 * 1.1 = 110.0 (a float, because 1.1 is a float).
  • Note the pattern: capture the parent result in a variable, then augment it. This is "extend, don't replace."

Level 3 — Analysis

Exercise 3.1

Trace the printed output for this diamond.

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()
Recall Solution 3.1

Prints D B C A (each once), then stops.

  • First write the MRO: D.__mro__ = (D, B, C, A, object). See the figure below — it is a single straight line, not the diamond shape you drew.
  • D.__init__: prints D, super() → next after D is B.
  • B.__init__: prints B, super() → next after B in D's MRO is C (not A!). This is the subtle point.
  • C.__init__: prints C, super() → next after C is A.
  • A.__init__: prints A, super() → next after A is object whose __init__ does nothing.
Figure — `super()` — calling parent methods

Exercise 3.2

What breaks here, and why?

class A:
    def __init__(self): print("A"); super().__init__()
class B(A):
    def __init__(self): print("B"); A.__init__(self)   # hard-coded!
class C(A):
    def __init__(self): print("C"); super().__init__()
class D(B, C):
    def __init__(self): print("D"); super().__init__()
 
D()
Recall Solution 3.2

Prints D B A — and C is silently SKIPPED.

  • DB. Good.
  • B prints B, then calls A.__init__(self) directly — jumping straight to A, bypassing C.
  • A prints A, its super().__init__() reaches object, done.
  • C.__init__ never runs. If C.__init__ set up an attribute, that attribute is missing → later AttributeError. This is the exact bug the parent note warns about: hard-coded Parent.__init__ breaks the MRO chain.

Level 4 — Synthesis

Exercise 4.1

Design cooperative mixins so that saving passes through both layers exactly once, in order. Fill in the blanks and give the printed output.

class Saver:
    def save(self, data):
        print("write", data)
 
class Encrypted(Saver):
    def save(self, data):
        print("encrypt", data)
        super().save(data)      # (1)
 
class Compressed(Saver):
    def save(self, data):
        print("compress", data)
        super().save(data)      # (2)
 
class Store(Encrypted, Compressed):
    pass
 
Store().save("hi")

Predict the four printed lines.

Recall Solution 4.1

Prints:

encrypt hi
compress hi
write hi
  • MRO: Store.__mro__ = (Store, Encrypted, Compressed, Saver, object).
  • Store has no save, so lookup starts at Encrypted.save: prints encrypt, then super()Compressed.
  • Compressed.save: prints compress, then super()Saver.
  • Saver.save: prints write, no super() call → chain ends.
  • Each layer runs once, in MRO order. This is the "cooperative super" pattern: every method does its bit, then delegates upward.

Exercise 4.2

Add a class AuditedStore(Store) that prints "audit" before everything else, reusing the existing chain. Show the four... five printed lines.

Recall Solution 4.2
class AuditedStore(Store):
    def save(self, data):
        print("audit", data)
        super().save(data)

Prints:

audit hi
encrypt hi
compress hi
write hi

MRO: (AuditedStore, Store, Encrypted, Compressed, Saver, object). Store has no save, so after AuditedStore the chain flows to Encrypted and continues exactly as before — we extended without touching any existing class.


Level 5 — Mastery

Exercise 5.1

Cooperative __init__ with arguments. Every class consumes the kwargs it understands and forwards the rest via super().__init__(**kwargs). What are p.name, p.role, p.level?

class Person:
    def __init__(self, name, **kw):
        self.name = name
        super().__init__(**kw)          # forwards nothing left → object
 
class Employee(Person):
    def __init__(self, role, **kw):
        self.role = role
        super().__init__(**kw)
 
class Manager(Employee):
    def __init__(self, level, **kw):
        self.level = level
        super().__init__(**kw)
 
p = Manager(level=3, role="lead", name="Ana")
print(p.name, p.role, p.level)
Recall Solution 5.1

Prints Ana lead 3.

  • MRO: (Manager, Employee, Person, object).
  • Manager.__init__(level=3, role="lead", name="Ana"): sets self.level = 3, then super().__init__(role="lead", name="Ana")Employee.
  • Employee.__init__: sets self.role = "lead", then super().__init__(name="Ana")Person.
  • Person.__init__: sets self.name = "Ana", then super().__init__()object.__init__() with no leftover kwargs → does nothing.
  • Every attribute is set exactly once. **kw is the trick that lets each level pluck its own argument and pass the rest along without knowing who is next.

Exercise 5.2

Predict the failure. Why does this raise, and at which line?

class Person:
    def __init__(self, name):
        self.name = name
        super().__init__()
 
class Employee(Person):
    def __init__(self, role, **kw):
        self.role = role
        super().__init__(**kw)
 
Employee(role="dev", name="Sam", extra="oops")
Recall Solution 5.2

Raises TypeError inside Person.__init__.

  • Employee.__init__ peels off role, forwards {name:"Sam", extra:"oops"}.
  • Person.__init__(self, name, ...) accepts name but has no **kw to absorb extra.
  • So Person.__init__ is called with an unexpected keyword extra="oops"TypeError: __init__() got an unexpected keyword argument 'extra'.
  • Lesson: in cooperative __init__, either every class accepts **kw, or the top of the chain must consume all remaining kwargs. A stray/misspelled kwarg surfaces exactly here.

Exercise 5.3

Two-word verify. For class D(B, C) with B(A), C(A), A(object), is the MRO [D, B, C, A, object] or [D, B, A, C, object]? State the C3 reason in one sentence.

Recall Solution 5.3

[D, B, C, A, object]. C3 linearization forbids a class from appearing before all of its subclasses are placed: A is a base of both B and C, so A cannot come until both B and C are already in the list. Hence A is pushed after C, giving D B C A object.


Recall Feynman recap: one sentence per level

L1 — super() already holds self, so never pass it. L2 — capture the parent result, then extend it. L3 — super() follows type(self)'s MRO, so it can jump to a sibling. L4 — cooperative super() lets each layer run once, in MRO order. L5 — forward leftover arguments with **kw so every class picks what it needs.

Connections