skip to content

What does a zero-argument super() call inside a method resolve to at runtime?

level: juniorimportance: must knowfreq 72%

answer

  1. Not a synonym for the parent class
  2. The answer depends on the receiver
  3. Two bindings: defining class and receiver
  4. Lookup starts one past the defining class
  5. In a diamond it crosses branches

basics

~10 s

A zero-argument super() builds a proxy that starts attribute lookup at the class after the one where the method was defined, following the receiver's method resolution order rather than the class's declared base.

solid answer

~40 s

`super()` is not a synonym for "my parent class". It returns a proxy bound to two things: the class the method was defined in, and the object it was called on. Attribute lookup on that proxy walks the receiver's `__mro__` and starts one position past the defining class, so which class it reaches depends on the runtime type of the receiver, not on the defining class's base list. Under single inheritance the two coincide, which is why the wrong mental model survives. In a diamond such as `D(B, C)` where both `B` and `C` derive from `A`, a `super().who()` written inside `B` dispatches into `C` when the receiver is a `D` — a class `B` never named. That is what makes a chain of `super()` calls cooperative: each class runs once, in one order.

code

python · 15 lines
python
class A:
    def who(self): return "A"

class B(A):
    def who(self): return "B->" + super().who()

class C(A):
    def who(self): return "C->" + super().who()

class D(B, C):
    def who(self): return "D->" + super().who()

print([c.__name__ for c in D.__mro__])
print(D().who())
print(B().who())

go deeper

for a junior

Be ready to state what super() does in the ordinary single-inheritance case and to write super().init() in a subclass initializer. Knowing that it delegates upward, and that you should not name the base class by hand, is the floor here.

for a middle

Explain the two bindings — defining class plus receiver — and that lookup starts one position past the defining class in the receiver's order. Expect to trace a four-class diamond on a whiteboard and point out where the call leaves the defining class's base list.

for a senior

Show the production consequence: one class that hard-codes its base call quietly removes every class between it and that base, so an initializer or a teardown step stops running with no error at all. Say how you would catch that in review or in a test.

for a principal

Own it as an API contract. If your hierarchy is meant to be extended by other teams, cooperative super() calls are part of what you publish, and a base that cannot be safely composed should be documented as final or replaced with delegation rather than left as a trap.

### The mistake the name encourages `super` reads like "the superclass", and in a single-inheritance tree that reading is never caught out. It is still wrong. A zero-argument `super()` call names no class at all. It builds a *proxy object* carrying two pieces of information: the class in which the executing method was defined, and the object the method was called on. Attribute lookup on that proxy walks the receiver's method resolution order — the flat tuple of classes in `type(receiver).__mro__`, fixed once when that class was created — and begins at the entry *after* the defining class. Because the walk uses the *receiver's* order, the class `super()` reaches depends on the runtime type of the object, which is not knowable when the method is written. ### The diamond that proves it ```python class A: def who(self): return "A" class B(A): def who(self): return "B->" + super().who() class C(A): def who(self): return "C->" + super().who() class D(B, C): def who(self): return "D->" + super().who() print([c.__name__ for c in D.__mro__]) # ['D', 'B', 'C', 'A', 'object'] print(D().who()) # D->B->C->A print(B().who()) # B->A ``` `B.who` contains exactly one `super().who()` call and it dispatches to two different classes. For a plain `B` instance the order is `(B, A, object)` and the entry after `B` is `A`. For a `D` instance the order is `(D, B, C, A, object)` and the entry after `B` is `C` — a class that appears nowhere in `B`'s bases and whose author `B` may never have met. That is the mechanism working as designed, not an accident: it is what lets `D` compose `B` and `C` into a chain in which every class runs exactly once, in one order, with `A` running once at the end. ### The two bindings Spelling the proxy out helps: read `super()` as "the defining class, plus this receiver". The defining class is a compile-time fact — the class whose body physically contains the method. The receiver is a run-time fact. Lookup then searches the slice of the receiver's order that follows the defining class, in order, through each class's own `__dict__`. Nothing about the defining class's base list enters that computation except through its position in the receiver's order. ### What cooperation demands The chain only holds if every class in it plays along. Each override must call `super()` rather than reaching for a named base, must accept the arguments its predecessors will pass, and must not assume it is last. If one class in the middle writes `A.who(self)` instead of `super().who()`, it hard-codes a jump to `A` and every class between it and `A` — `C`, in the example above — silently stops running. In an initializer chain the classic symptom is worse than a skip: when several branches each call a shared base explicitly, that base runs once per branch, so setup happens twice and the second run clobbers the first. ### It is not only about methods The proxy resolves any *class* attribute, running the descriptor protocol as it goes: `super().p` invokes the next `property` getter in the order, `super().x` reads the next class-level constant, and inside a `classmethod` a zero-argument `super()` binds the class rather than the instance. What it will never see is instance state — attributes assigned in `__init__` live in the instance `__dict__`, which the proxy skips entirely, because there is no "parent copy" of an instance to look in. ### When the search runs out If no class after the defining class supplies the name, the lookup fails like any other missing attribute: `AttributeError: 'super' object has no attribute ...`. `object` sits at the end of every order and defines very little, so a `super().save()` whose remaining order contributes no `save` raises rather than quietly doing nothing. That matters for optional hooks: if a step in the chain may or may not exist downstream, guard the call explicitly instead of assuming it degrades to a no-op. It is also the reason a truncated chain is so hard to notice — skipping a class that would have run is silent, while reaching the end of the order is loud. ### How to say it in an interview Two sentences carry the whole answer. `super()` answers "who is next", not "who is my parent". And the answer is a property of the instance, so a method written in one class can, and routinely does, dispatch into a class it has never heard of. Everything else — cooperative initializers, composable behaviour classes, the rule against naming a base directly — follows from those two facts.

  • Does super() resolve anything other than methods?
    Yes. The proxy performs a normal class-attribute lookup over the remaining part of the order and honours the descriptor protocol, so `super().p` calls the next `property` getter, `super().x` reads the next class-level constant, and a zero-argument `super()` inside a `classmethod` binds the class instead of the instance. The one thing it cannot reach is instance state: attributes set in `__init__` live in the instance `__dict__`, which the proxy never searches.
  • What breaks if a class in a diamond calls A.who(self) explicitly instead of super().who()?
    It hard-codes the jump and cuts out everything between. In `D(B, C)`, a `B.who` that calls `A.who(self)` skips `C` entirely, so whatever `C` contributes never happens. The dual failure is duplication: if two branches each call the shared base directly, that base runs once per branch. Explicit base calls are only safe in a single-inheritance tree you fully control and never expect anyone to extend.
  • If super() depends on the receiver, is the order recomputed on every call?
    No. The order is a tuple computed once, when the class object is created, and cached on the class; `super()` only indexes into it. That is why the dispatch is cheap and deterministic — two calls on the same receiver always take the same path. Changing the shape of a hierarchy means creating a new class, not mutating the order of an existing one.

The method resolution order is a relay lane fixed when the class is created; super() hands the baton to whoever is standing next in that lane, which need not be the runner your class was written behind.

saying these in an interview costs you the question

  • Says super() always calls the declared parent class
  • Believes super() returns the base class object itself
  • Thinks the target is fixed by the defining class's bases
  • Calls the base explicitly, as A.method(self), in a diamond
  • Expects super() to reach attributes set in __init__
  • Assumes multiple inheritance means a base runs twice

context