The super() Function
super() returns a proxy object that routes attribute lookups to the next class in the MRO - not "the parent" in the family-tree sense, but the next entry in the resolution order. The zero-argument form (super() inside a method) is the one you will write 99% of the time: it means "from here, continue the chain." Used well, it is what makes inheritance cooperative - a child can extend a parent's work without copy-pasting the parent's code, and a mixin chain can flow through every base in order.
The Core Use: Chaining __init__
class Animal:
def __init__(self, name):
self.name = name
class Mammal(Animal):
def __init__(self, name, warm=True):
super().__init__(name) # run Animal's setup first
self.warm = warm
class Dog(Mammal):
def __init__(self, name, breed="mixed"):
super().__init__(name) # runs Mammal, which runs Animal
self.breed = breed
d = Dog("Rex", breed="border")
print(d.name, d.warm, d.breed) # Rex True border
print(Dog.mro()) # [Dog, Mammal, Animal, object]
The rule for __init__: call super().__init__() with the arguments the next class documents, so each class in the chain performs its own setup. The order matters - call super() first when the parent sets up state your own code depends on (the normal case), and last when you need to compute something the parent's constructor consumes. Forgetting the call is the classic inheritance bug: the parent's attributes never exist, and the failure surfaces wherever they are first read, often far from the cause. A linter that flags __init__ methods not calling super().__init__ (with documented exceptions) catches the most common case.
super() Is Not "My Parent"
class A:
def show(self):
print("A")
class B(A):
def show(self):
print("B")
super().show()
class C(A):
def show(self):
print("C")
class D(B, C):
def show(self):
print("D")
super().show()
D().show()
# D
# B
# C
# A
In D(B, C), the MRO is [D, B, C, A, object]. Inside B.show, super() resolves to "the next class after B in this instance's MRO," which is C - not A, even though A is B's direct parent. The proxy starts the search after the defining class and follows the actual MRO of the object being used. This is what makes cooperative chains work: each class calls super() once, and the MRO guarantees the full sequence. It is also why a base written for single inheritance (one that assumes super() lands on object and stops) can surprise when mixed into a diamond - the chain continues past where the author expected.
The Two-Argument Form
class Base:
def greet(self):
return "base"
class Child(Base):
def greet(self):
# explicit form: super(cls, instance) - equivalent to super() in a method
return super(Child, self).greet() + " / child"
print(Child().greet()) # 'base / child'
# the explicit form matters OUTSIDE an implicit method context:
class MetaDemo:
def helper(self):
# no enclosing method's __class__ to infer from - name them
return super(MetaDemo, self).greet() if hasattr(self, "greet") else "n/a"
super(cls, obj) is the explicit spelling: start the MRO search after cls, using obj's actual MRO. Inside a regular method, super() is equivalent to super(__class__, self) - Python fills both in from the enclosing function's __class__ cell (which is why a class defined in a comprehension or lambda can surprise you with a NameError for __class__). You will see the two-argument form in descriptors, metaclass methods (where the "instance" is a class), and code that must be robust to being copied out of its original method context.
Why a Proxy, and the Cooperativity Contract
super() returns a proxy, not the parent class itself: attribute access on it starts the MRO search after the current class. The design serves one contract - cooperativity: every class in a chain either (a) does its work and calls super() to hand off, or (b) deliberately terminates the chain. A well-behaved mixin follows (a) for every method it overrides; a well-behaved leaf may follow (b). When all bases are cooperative, the MRO makes the full chain run exactly once per class - no duplicates, no skips. When one base breaks the contract (forgets super(), or calls it twice), the chain misbehaves in a way the MRO itself cannot fix: the order is correct, the participation is not. That is why the discipline - one super() call per overridden method, always - is the real content of this chapter.
Pitfalls
- Calling
super()with the wrong class -super(Animal, self)insideDogstarts afterAnimal, skippingMammal. Name the class you are in, or use the zero-arg form. - Hard-coding
Parent.method(self, ...)instead ofsuper()- it works in a single-parent world and breaks the moment the class is mixed;super()is the MRO-correct spelling. - super() in a staticmethod or at module level - the zero-arg form needs an instance context; use the two-arg form or restructure.
- Assuming super() reaches object and stops. In a diamond it reaches the next base; write bases that are happy to be in a chain (call super, return its result) if they might be.
Summary
super() hands attribute lookups to the next class in the MRO - a proxy, not a parent reference - and the zero-argument form is the idiom for cooperative inheritance: call super().__init__() first in constructors, and in any overridden method do your work then call super() once to continue the chain. It is not "my direct parent" (in a diamond it is the next MRO entry), the explicit super(cls, obj) form covers contexts where the implicit one cannot infer its arguments, and the cooperativity contract - every base either hands off or intentionally stops - is what makes multiple-inheritance chains run each class exactly once. Master the one rule (one super call per override, named correctly) and the rest of the inheritance chapters stop having surprises.