2.1.9 · D5OOP Fundamentals
Question bank — `super()` — calling parent methods
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]]