PYTHON / OBJECT-ORIENTED PYTHON
super() and the method resolution order
Read a class's __mro__, predict which implementation zero-arg super() calls in a diamond, and chain __init__ cooperatively so every class runs.
What you will learn
- Inspect a linearization with Cls.__mro__ and know lookup walks it left to right
- Predict what super() reaches: the class after you in type(self).__mro__
- Forward **kwargs through super().__init__ so every class in a diamond initialises
- Diagnose 'cannot create a consistent MRO' TypeErrors by reordering the bases
Understanding super() and the method resolution order
Every class carries a precomputed search list called its method resolution order, exposed as Cls.__mro__ (a tuple) or Cls.mro() (a list). Attribute lookup on an instance checks the instance dictionary first, then walks that list left to right and stops at the first class whose __dict__ contains the name. Python builds the list with the C3 algorithm, which guarantees that a class appears before all of its own bases and that the left-to-right order you wrote in the class statement is preserved.
super() does not mean "my parent class". It creates a proxy object holding two things: the class in which the call is written, and the instance being operated on. Lookups on that proxy begin at the position immediately after that class in type(self).__mro__, so the target depends on the runtime type of the instance, not on the bases of the class you are editing. The zero-argument form works because the compiler stashes the enclosing class in a hidden __class__ cell and reuses the method's first parameter.
That indirection is exactly what makes multiple inheritance usable: each class does its own part and delegates the remainder with super(), without naming who comes next. The price is cooperation. If one class in the middle forgets its super() call, everything after it in the MRO is silently skipped and no error is raised; if signatures disagree, arguments land in a sibling that cannot accept them. Passing leftovers along as **kwargs is the standard way to keep such a chain reorderable.
class Base:
def greet(self):
return "Base"
class Left(Base):
def greet(self):
return "Left -> " + super().greet()
class Right(Base):
def greet(self):
return "Right -> " + super().greet()
class Both(Left, Right):
def greet(self):
return "Both -> " + super().greet()
print([c.__name__ for c in Both.__mro__])
print(Both().greet())
print(Left().greet())super() resolves to the next class after the current one in type(self).__mro__, making it cooperative delegation rather than a reference to a parent class.
Worked examples
Cooperative __init__ with **kwargs
Every class consumes the keywords it understands and forwards the rest, so all four __init__ bodies run once.
class Widget:
def __init__(self, **kwargs):
self.extra = kwargs
super().__init__()
class Sized:
def __init__(self, width=0, **kwargs):
self.width = width
super().__init__(**kwargs)
class Colored:
def __init__(self, color="none", **kwargs):
self.color = color
super().__init__(**kwargs)
class Button(Sized, Colored, Widget):
def __init__(self, label, **kwargs):
self.label = label
super().__init__(**kwargs)
b = Button("OK", width=40, color="red", tooltip="press me")
print(b.label, b.width, b.color, b.extra)
print([c.__name__ for c in Button.__mro__])Example explained
Line 1Sized and Colored do not inherit from each other, yet super() inside Sized reaches Colored because that is what follows Sized in Button's MRO.
Line 2Each __init__ pops its own keyword out of the signature and passes **kwargs on, so no class needs to know the arguments of the next one.
Line 3Widget is the end of the chain: it swallows the leftovers and calls super().__init__() with no arguments, because object.__init__ rejects extras.
Line 4Swapping Sized and Colored in the bases changes the MRO and the call order, but the code still works unchanged.
When no consistent MRO exists
C3 refuses to linearize bases whose order contradicts an existing inheritance relationship.
class A:
pass
class B(A):
pass
try:
class C(A, B):
pass
except TypeError as e:
print("TypeError:", " ".join(str(e).split()))
class D(B, A):
pass
print([c.__name__ for c in D.__mro__])Example explained
Line 1class C(A, B) demands that A come before B, but B is a subclass of A and must precede it, so the two rules conflict.
Line 2The error is raised while the class statement executes, not when a method is called, so this kind of mistake fails fast.
Line 3" ".join(str(e).split()) collapses the message, which CPython prints across two lines.
Line 4class D(B, A) states the same relationship the inheritance already implies, so it linearizes to D, B, A, object.
The explicit two-argument form
super(Cls, obj) lets you choose the starting point in the MRO, including one that skips an override.
class A:
def who(self):
return "A"
class B(A):
def who(self):
return "B"
class C(B):
def who(self):
return "C"
c = C()
print(c.who())
print(super(C, c).who())
print(super(B, c).who())
print(C.__mro__.index(B))Example explained
Line 1super(C, c) starts the search after C in type(c).__mro__, so it finds B.who.
Line 2super(B, c) starts after B, which skips B.who entirely and reaches A.who.
Line 3The second argument must be an instance (or subclass) of the first, otherwise Python raises TypeError: super(type, obj): obj must be an instance or subtype of type.
Line 4C.__mro__.index(B) confirms B sits at position 1, right where the first super() call resumed.
Important notes
Zero-argument super() depends on compiler support: it needs the hidden __class__ cell of the enclosing class body and the method's first positional argument. In a staticmethod, a lambda, or a nested helper function there is no such argument, so use super(Cls, obj) there.
object.__init__ accepts no extra arguments, so the class that terminates a cooperative chain decides the policy: swallow unknown keywords yourself, or forward them and let object turn a misspelled keyword into a TypeError.
Common mistakes
Writing Base.__init__(self, ...) instead of super().__init__(...): in a diamond the sibling class between you and Base is skipped, and Base can end up running twice.
Omitting the super() call in one middle class: nothing raises, but every class after it in the MRO never runs, so an attribute it would have set shows up later as an AttributeError.
Assuming super() is the first base you listed: when someone adds a mixin, super() suddenly targets that mixin, and positional arguments meant for the old parent produce a TypeError about unexpected arguments.
Try it yourself
Change, predict, then run
Define Base with save() returning "raw", plus Zip and Encrypt that each override save() by wrapping super().save() in their own text, then Archive(Zip, Encrypt, Base); print the MRO names and Archive().save(), then swap Zip and Encrypt in the base list and confirm the wrapping order flips.
Open the Python workspaceCheck your understanding
A defines ping(). B(A) and C(A) each override ping() and call super().ping(). D(B, C) inherits both. D().ping() runs B.ping, then C.ping, then A.ping. Why does super() inside B reach C?
- Because super() resumes after B's position in type(self).__mro__, and for a D instance that list is D, B, C, A, object
- Because super() always calls the first base listed in the current class's own bases
- Because Python calls every ping() it can find in the hierarchy, in definition order
- Because defining D(B, C) makes B a subclass of C behind the scenes
Show answer
The super() proxy stores the class where the call is written (B) and the instance, then continues from B's slot in the MRO of the instance's actual type (D), which puts C next. Option 2 is the tempting one, but B's only base is A, and A.ping runs last, after C - so super() clearly is not consulting B's own bases.