2.1.9 · D5OOP Fundamentals

Question bank — `super()` — calling parent methods

1,631 words7 min readBack to topic

Before we start, one shared vocabulary so nothing below uses a word you haven't been handed:

Keep this mental picture: super() is not "my parent." It is "the class listed right after me in type(self).__mro__." In simple family trees those are the same thing — and that coincidence is exactly what breeds the misconceptions below.


True or false — justify

Every answer explains why, never a bare yes/no.

super() always refers to the direct parent class.
False. It refers to the next class in type(self).__mro__ after the current one — in multiple inheritance that "next" class can be a sibling, not a parent. See Multiple Inheritance & the Diamond Problem.
Inside Dog.__init__, super() is equivalent to super(Dog, self).
True. The zero-argument form is compiler sugar: Python fills in the enclosing class and the first argument automatically, so super() and super(Dog, self) behave identically.
You must pass self when calling super().__init__(name).
False. The proxy already bound self; adding it produces TypeError: got multiple values for argument. super() has no self in the call.
Animal.__init__(self, name) and super().__init__(name) always do the exact same thing.
False. They match only in single inheritance. In a diamond, the hard-coded call fixes which class runs next, breaking the MRO chain and possibly running the base twice.
If a child does not override __init__ at all, you still need to call super() somewhere.
False. If the child defines no __init__, Python uses the parent's __init__ directly via the MRO — there's nothing to extend, so no super() call is needed.
super() can only be used inside __init__.
False. It works in any method — save, __str__, custom methods — anywhere a child wants to extend rather than replace the inherited version (see Worked Example 2 in the parent).
Calling super().__init__() guarantees the parent's __init__ runs.
False (subtly). It runs the next class in the MRO's __init__, which in multiple inheritance may be a sibling, not the class you were picturing as "parent."
In class D(B, C) where B(A) and C(A), class A runs twice if every class uses super().
False. The MRO D, B, C, A, object lists A exactly once, so A.__init__ runs exactly once — that single-run guarantee is super()'s headline feature.
super() walks up a tree.
Misleading. It walks along a flat list (the MRO), not up a branching tree. The tree gets flattened first; super() just steps to the next entry.

Spot the error

Each item shows code that looks correct. Name the bug and the fix.

class Dog(Animal):
    def __init__(self, name):
        super().__init__(self, name)
``` ::: `self` is passed twice — the proxy already carries it. Remove it: `super().__init__(name)`. The error surfaces as "got multiple values for argument".
 
```python
class Dog(Animal):
    def __init__(self, name, breed):
        self.breed = breed
        super().__init__(name)
``` ::: Not an error, but a **fragile ordering**: usually you call `super().__init__` *first* so parent attributes exist before the child's logic (which might read them). Reorder unless you deliberately need child state set up first.
 
```python
class Dog(Animal):
    def __init__(self, name, breed):
        Animal.__init__(name)
``` ::: `Animal.__init__` is called **unbound**, so `name` lands in the `self` slot and no `self` is supplied. Either `Animal.__init__(self, name)` or, better, `super().__init__(name)`.
 
```python
class Dog(Animal):
    def __init__(self, name, breed):
        self.breed = breed   # no super call at all
``` ::: The parent's setup (`self.name`, `self.legs`) never runs, so those attributes don't exist → `AttributeError` on first access. Overriding means *add*, not *erase the setup*.
 
```python
class D(B, C):
    def __init__(self):
        B.__init__(self)
        C.__init__(self)
``` ::: Hard-coding both bypasses the MRO. In a diamond over `A`, `A.__init__` now runs **twice** (once via `B`, once via `C`). Use `super().__init__()` once so the MRO visits `A` a single time.
 
```python
class TimestampLogger(Logger):
    def save(self, data):
        print("saved", data)
        # forgot to reuse parent validation
``` ::: The parent's validation is silently dropped, so `TimestampLogger.save` *replaces* rather than *extends*. Add `ok = super().save(data)` to keep the single source of truth.
 
---
 
## Why questions
 
Why does `super()` deliberately start searching *after* the current class instead of at the top of the MRO? ::: So the chain moves *forward* one step at a time; starting at the top would re-run the current class and loop forever. "After me" is what lets each class delegate exactly once down the line.
 
Why is `super()` safer than `Parent.__init__(self)` when someone later inserts a new class into the hierarchy? ::: `super()` re-reads the *live* MRO of `type(self)` at call time, so it automatically routes through the newly inserted class. A hard-coded name knows nothing about the new class and skips it. This is the decoupling benefit from [[Inheritance — extending classes]].
 
Why must `super()` use `type(self).__mro__` rather than the MRO of the class where the code was written? ::: Because an instance might be of a *subclass*. The MRO that matters is the actual object's, so a grandchild's siblings still get visited — that's what makes cooperative multiple inheritance work.
 
Why does the zero-argument `super()` need to know its enclosing class at all? ::: To find its own position in the MRO ("start after *me*"). Python captures the enclosing class in a hidden cell (`__class__`) at compile time, which is why bare `super()` only works *inside* a class body's method.
 
Why can passing extra keyword arguments through `super().__init__(**kwargs)` matter in a diamond? ::: In cooperative multiple inheritance, each class consumes the args it needs and forwards the rest, so the chain reaches `object.__init__` cleanly. Swallowing everything breaks siblings further down the MRO. Related design tension: [[Composition over Inheritance]].
 
Why is `super()` described as returning a *proxy* rather than the parent class itself? ::: Because the returned object doesn't own the methods — it forwards each lookup into the MRO, binding `self` for you. It's a router, not a class.
 
---
 
## Edge cases
 
What does `super().__init__()` do when the next class in the MRO is `object`? ::: `object.__init__()` runs — it does essentially nothing but terminates the cooperative chain cleanly, which is why it's safe (and expected) to reach it at the bottom of the MRO.
 
What happens if you call `super()` inside a plain function that is *not* a method of any class? ::: `RuntimeError: super(): no arguments` — the zero-arg form has no enclosing class cell to read, so it cannot compute "after me."
 
In `class D(B, C)`, if `B` overrides a method but `C` does not, does `super()` from `D` still reach `C`? ::: Yes if `B`'s override calls `super()`. The call chain steps `D → B → C → A`, so `C` is visited even though it didn't override — provided every link cooperates by calling `super()`.
 
If one class in a multiple-inheritance chain *forgets* to call `super()`, what breaks? ::: The chain is cut at that class: every class *after* it in the MRO is skipped. This is why cooperative `super()` requires *every* participating method to forward the call.
 
Does `super()` in a `@classmethod` behave the same as in an instance method? ::: Yes in spirit — it delegates to the next class in the MRO — but it binds the *class* rather than an instance, so you call `super().classmethod_name(...)` and the `cls` is injected instead of `self`.
 
Can `super()` return different "next classes" for the *same* class depending on the object? ::: Yes. Because it uses `type(self).__mro__`, an instance of a subclass gives a longer MRO, so `super()` in a shared base can route to different siblings for different object types — the core mechanism of cooperative inheritance.
 
What is the MRO's role when two parents define the *same* method name (no diamond)? ::: The MRO's left-to-right order decides which one `super()` reaches first; the leftmost parent in the class definition `class D(B, C)` wins, so `B`'s version is found before `C`'s.
 
Is it ever correct to call `Parent.method(self)` directly instead of `super()`? ::: Occasionally — when you *intentionally* want one specific ancestor's version and are certain there's no diamond to disturb. But it's a deliberate exception; the default should be `super()` to keep the MRO intact.
 
---
 
> [!recall]- One-sentence trap-buster
> Whenever you catch yourself saying "`super()` means the parent," replace it with "`super()` means the **next class in `type(self)`'s flat MRO**" — nearly every trap on this page dissolves once you make that swap.
 
## Connections
- [[2.1.09 `super()` — calling parent methods (Hinglish)]]
- [[Inheritance — extending classes]]
- [[Method Resolution Order (MRO) & C3 Linearization]]
- [[Method Overriding vs Overloading]]
- [[Multiple Inheritance & the Diamond Problem]]
- [[`__init__` — constructors]]
- [[Composition over Inheritance]]