How does `super()` work in Python, and why is the zero-argument form preferred over `super(ClassName, self)`?
`super()` returns a proxy that delegates method calls to the next class in the MRO, not necessarily the direct parent. The zero-argument form `super()` (Python 3) is preferred because it uses a compiler-injected `__class__` cell, which correctly tracks the defining class even under multiple inheritance — avoiding the brittle repetition of the class name.
How to think about it
The interviewer is checking whether you know that super() is not a synonym for “call the parent.” It’s about the Method Resolution Order — the linearized list of classes Python searches for a method. In single inheritance the distinction is invisible; under a diamond-shaped hierarchy it’s everything.
Start with the easy case, because it builds the intuition. In single inheritance super().__init__() really does just call the parent:
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # delegates to Animal.__init__
self.breed = breed
Everyone reads that correctly. The interesting case is multiple inheritance, where “the parent” stops being well-defined.
A worked example
When D inherits from B and C, and both extend A, Python must run A.setup exactly once. The MRO is [D, B, C, A, object], and each class’s super() steps to the next entry — not to its own parent:
class A:
def setup(self):
print("A.setup")
class B(A):
def setup(self):
print("B.setup"); super().setup() # next after B is C, not A
class C(A):
def setup(self):
print("C.setup"); super().setup() # next after C is A
class D(B, C):
def setup(self):
print("D.setup"); super().setup()
print("MRO:", [c.__name__ for c in D.__mro__])
print()
D().setup()
MRO: ['D', 'B', 'C', 'A', 'object']
D.setup
B.setup
C.setup
A.setup
Follow the chain: D calls super(), which lands on B; B’s super() lands on C — not A, even though C is nowhere in B’s own bases; C’s super() finally reaches A. Each class runs once, in MRO order. Take the super() calls out of B and C and you’d skip classes or run A.setup twice.
Why the zero-argument form
super().__init__(name) # Python 3 — preferred
super(Dog, self).__init__(name) # explicit — error-prone
The explicit form hardcodes the class name. Rename Dog, copy the method into a subclass, or reshuffle the hierarchy, and that frozen name breaks — sometimes silently, sometimes as a baffling runtime TypeError. The bare super() instead reads __class__, a read-only cell the compiler injects at method-definition time; it always means “the class that textually defines this method,” no matter what self happens to be at runtime.