Object model3 min read

Python super() Follows the MRO, Not the Parent Class

How Python super() advances through the method resolution order and how to design cooperative multiple inheritance.

  • inheritance
  • super
  • method resolution order

super() does not mean “call my parent.” It creates a proxy that searches the receiver’s method resolution order, starting immediately after a specified class. In an ordinary instance method, zero-argument super() supplies the class where the method was defined and the method’s first argument.

This distinction matters as soon as a class participates in multiple inheritance. It lets every implementation call the next implementation exactly once, including a sibling reached through a diamond. The behavior described here matches Python 3.14.7’s super() documentation.

Read the order from __mro__

Python computes one linear order for attribute lookup. Since Python 2.3, class creation has used the C3 algorithm, which preserves the local left-to-right order of bases and keeps inherited precedence monotonic. Python rejects a hierarchy when it cannot produce a consistent order. The official MRO guide gives the full merge algorithm.

class Root:
    def save(self):
        print("Root")

class Validate(Root):
    def save(self):
        print("Validate")
        super().save()

class Audit(Root):
    def save(self):
        print("Audit")
        super().save()

class Service(Validate, Audit):
    def save(self):
        print("Service")
        super().save()

print([cls.__name__ for cls in Service.__mro__])
Service().save()

The output is:

['Service', 'Validate', 'Audit', 'Root', 'object']
Service
Validate
Audit
Root

Inside Validate.save, the receiver is still a Service. The search therefore uses Service.__mro__ and continues after Validate, which reaches Audit. Replacing that call with Root.save(self) would skip Audit and hard-code one hierarchy into a reusable class.

Cooperation requires compatible methods

super() supplies routing, not argument adaptation. Methods that share one cooperative chain need compatible signatures. Initializers often consume their own keyword arguments and pass the rest onward:

class Named:
    def __init__(self, *, name, **kwargs):
        self.name = name
        super().__init__(**kwargs)

class Retried:
    def __init__(self, *, retries=0, **kwargs):
        self.retries = retries
        super().__init__(**kwargs)

class Job(Named, Retried):
    pass

job = Job(name="backup", retries=3)
assert (job.name, job.retries) == ("backup", 3)

Each method consumes only the arguments it owns. object.__init__ ends the chain and accepts no leftover configuration, so a misspelled or unclaimed keyword raises TypeError instead of disappearing.

Calling the next method is also part of the contract. One implementation that omits super() stops the chain. Calling it twice repeats every later implementation. Mixins intended for cooperative use should document the shared signature and call super() once on every normal path.

Zero-argument super() uses compiler context

The compiler creates an implicit __class__ closure cell for a method that refers to super or __class__. Zero-argument super() combines that class with the immediately enclosing function’s first parameter. The class-creation reference documents this cell.

That convenience does not carry into a nested function or generator expression:

class Child(Root):
    def save_later(self):
        def callback():
            return super(Child, self).save()
        return callback

Use the explicit two-argument form in the nested function. A bare super() there examines the nested function’s frame, not the method frame that supplied self, and fails.

Direct base calls are a different tool

Calling Base.method(self) is appropriate when the requirement is specifically to invoke that implementation, regardless of the receiver’s MRO. It is not cooperative dispatch. Such a call can execute a shared ancestor twice in a diamond or bypass another mixin entirely.

For extensible hierarchies, inspect ConcreteClass.__mro__, keep signatures compatible, and let each method advance once. The current class determines where the search resumes. The receiver’s class determines the order being searched.