super() is often taught as "call my parent." That explanation survives single inheritance and fails exactly when super becomes most valuable. It does not select a parent, and it does not necessarily return a method defined above the current class in the source file. It creates a proxy whose lookup starts after a particular class in a particular method resolution order (MRO).

That distinction turns multiple inheritance from a collection of special cases into one algorithm. Every cooperative implementation performs its work and delegates to the next implementation selected by the runtime order. The class that comes next can change when a new subclass combines the participants.

Python guarantee. New-style classes use a consistent MRO, exposed through __mro__ and mro(). super(type, object) searches that order after type. The language specifies the behavior, not a sequence of dictionary reads.

Version note. These examples target Python 3.10 through 3.14 and were run on CPython 3.14.7. Zero-argument super, the __class__ closure, and two-argument proxy behavior are language features. CPython's proxy structure, adaptive bytecode, and lookup caches are implementation details.

Experiment 1: the next class is not always the parent

Pyodide / WebAssembly
class Root:
    def report(self):
        return ["Root"]


class Left(Root):
    def report(self):
        return ["Left", *super().report()]


class Right(Root):
    def report(self):
        return ["Right", *super().report()]


class Leaf(Left, Right):
    pass


print("[state] Leaf MRO:", [cls.__name__ for cls in Leaf.__mro__])
print("[result] cooperative report:", Leaf().report())

The order is Leaf, Left, Right, Root, object, and the report is Left, Right, Root. Inside Left.report, super() advances past Left in the MRO of the actual Leaf instance. It therefore finds Right.report, even though Right is not a base of Left.

Directly writing Root.report(self) would skip Right. Writing Left.__bases__[0].report(self) has the same defect with more machinery. Cooperative dispatch deliberately leaves the next participant open.

C3 gives one monotonic order

Python computes an MRO using C3 linearization. It preserves local base order, keeps each base's own precedence constraints, and is monotonic: deriving another class cannot reverse an established relationship. Python rejects combinations for which no consistent linearization exists rather than silently choosing an unstable order.

You rarely need to perform C3's merge by hand. Read SomeClass.__mro__ when debugging. The engineering requirement is stronger: methods participating in a cooperative chain must be prepared for whichever compatible implementation follows them.

Experiment 2: inconsistent orders fail at class creation

Pyodide / WebAssembly
class A:
    pass


class B:
    pass


class AB(A, B):
    pass


class BA(B, A):
    pass


try:
    class Impossible(AB, BA):
        pass
except TypeError as error:
    print("[error] class creation:", type(error).__name__)
    print("[check] mentions consistent MRO:", "consistent" in str(error))

AB requires A before B; BA requires B before A. No result can preserve both constraints. This TypeError is guaranteed behavior for an inconsistent hierarchy, although error wording is version-specific.

Treat a complicated MRO as design feedback. A valid linearization only proves that Python can order the classes. It does not prove their state assumptions, side effects, or method contracts compose sensibly.

What zero-argument super captures

In an ordinary method body, super() is equivalent in purpose to super(__class__, first_argument). The compiler creates a hidden __class__ closure cell when needed. The first argument is normally self or cls; the proxy validates the relationship and binds descriptors it finds.

Zero-argument super() is intentionally lexical. Copying a function that contains it onto another unrelated class does not rewrite the captured class. Nested functions and generator expressions can also lack the expected first argument. Use the explicit form when lexical inference is unavailable, not merely because it looks more detailed.

Experiment 3: inspect the hidden class cell

class Base:
    def name(self):
        return "base"


class Child(Base):
    def name(self):
        return super().name()


print(Child.name.__code__.co_freevars)
print(Child.name.__closure__[0].cell_contents is Child)
print(Child().name())

On CPython 3.14 this shows ('__class__',), True, and base. The observable success of zero-argument super() is portable; code-object and closure inspection exposes implementation-level artifacts and should remain diagnostic code.

Cooperative initialization is a protocol

The diamond matters most around __init__. Every participant should consume the arguments it owns, accept the rest, and call super().__init__ once. A terminal implementation must accept what remains. Keyword-only parameters make ownership explicit and avoid positional collisions.

Experiment 4: each initializer runs once

Pyodide / WebAssembly
class Endpoint:
    def __init__(self, **kwargs):
        if kwargs:
            raise TypeError(f"unused arguments: {sorted(kwargs)}")
        self.events = ["endpoint"]


class Named(Endpoint):
    def __init__(self, *, name, **kwargs):
        super().__init__(**kwargs)
        self.name = name
        self.events.append("named")


class Retrying(Endpoint):
    def __init__(self, *, retries=3, **kwargs):
        super().__init__(**kwargs)
        self.retries = retries
        self.events.append("retrying")


class Client(Named, Retrying):
    pass


client = Client(name="billing", retries=5)
print("[event] initializer order:", client.events)
print("[state] client name and retries:", client.name, client.retries)

The chain is Named -> Retrying -> Endpoint; each layer consumes one keyword. The endpoint rejects misspellings rather than quietly dropping them. Calling object.__init__ as the sink is also reasonable only after all arguments are consumed, because it accepts no application parameters.

This pattern is not a universal instruction to add **kwargs to every initializer. It is a protocol for a hierarchy intentionally designed for cooperative extension. In a closed single-inheritance tree, explicit signatures can be clearer and better for type checkers.

Experiment 5: one non-cooperative method breaks the chain

Pyodide / WebAssembly
class Root:
    def save(self):
        return ["root"]


class Audit(Root):
    def save(self):
        return ["audit", *super().save()]


class Validate(Root):
    def save(self):
        return ["validate"]  # Deliberately does not delegate.


class Record(Audit, Validate):
    pass


print("[result] save chain:", Record().save())

The result omits root. Cooperative multiple inheritance is only as cooperative as every implementation. A method may intentionally terminate a chain, but that must be part of the contract. Accidental omission is especially dangerous when delegation performs cleanup, registration, or security checks rather than adding visible list entries.

Code review should look for three properties: compatible signatures, one delegation call on every normal path, and no hard-coded class calls that jump over peers.

The two-argument form is a movable lookup cursor

super(Current, instance) returns a proxy bound to instance and positioned after Current. Current need not be type(instance); it must appear in the instance type's MRO. Attribute access on the proxy still invokes descriptors, so methods become bound correctly.

Experiment 6: move the cursor deliberately

Pyodide / WebAssembly
class A:
    def token(self):
        return "A"


class B(A):
    def token(self):
        return "B"


class C(B):
    def token(self):
        return "C"


item = C()
print("[result] lookup after C:", super(C, item).token())
print("[result] lookup after B:", super(B, item).token())
print("[state] proxy type after A:", super(A, item).__class__.__name__)

The first two calls produce B and A. The final expression examines the proxy's type rather than finding an application method after A. Explicit super is useful in metaprogramming and class methods, but using it to skip a known implementation usually signals a violated hierarchy contract.

Class methods cooperate too

The proxy can bind against a class instead of an instance. In a class method, zero-argument super() uses the defining class as the cursor and the runtime subclass as the bound object. Constructors and alternate factories can therefore preserve subclassing.

Experiment 7: a cooperative class method keeps the runtime class

Pyodide / WebAssembly
class Parser:
    @classmethod
    def from_text(cls, text):
        obj = cls()
        obj.parts = text.split(",")
        return obj


class StrippingParser(Parser):
    @classmethod
    def from_text(cls, text):
        return super().from_text(text.strip())


class CSV(StrippingParser):
    pass


result = CSV.from_text(" a,b ")
print("[result] runtime type and parsed parts:", type(result).__name__, result.parts)

Parser.from_text receives CSV as cls, not Parser. Replacing super() with Parser.from_text(...) would bind the base class and construct the wrong type unless manually unwrapped and rebound.

super does attribute lookup, not only method calls

Properties and other descriptors also work through the proxy. Assignment is different: super().x = value assigns to the proxy and generally does not mean "invoke the next setter." Call the descriptor through an appropriate design rather than treating super as a transparent instance.

Experiment 8: descriptors bind through the proxy

Pyodide / WebAssembly
class Base:
    @property
    def label(self):
        return "base"


class Decorated(Base):
    @property
    def label(self):
        return f"<{super().label}>"


print("[result] decorated label:", Decorated().label)

The base property's __get__ receives the Decorated instance, yielding <base>. This is the same descriptor machinery that binds methods; super changes where class-level search begins, not the object used for binding.

Engineering guidance

Use cooperative inheritance when classes genuinely refine one behavioral protocol and independent mixins can honor the same method contract. Keep mixins narrow, avoid hidden constructor requirements, document whether methods must delegate, and test combined MROs rather than testing every mixin only in isolation.

Prefer composition when components have independent lifecycles, need arbitrary ordering, expose incompatible signatures, or represent services rather than kinds of the same object. An explicit pipeline of validators is easier to configure than seven validate mixins. Do not use inheritance merely to borrow implementation; a helper function or contained object keeps dispatch visible.

Avoid no-argument super() outside a normal method body. Never hard-code a base call in a cooperative method. Do not assume the textual leftmost base handles the whole operation: it is only the first cursor position. Finally, inspect __mro__ in diagnostics and tests; guessing from a class diagram is unnecessary.

Reviewing a hierarchy as a call chain

An MRO listing is necessary but not sufficient evidence. For every cooperative operation, write down the signature and side effects of each implementation in order. Check that required state exists before a participant reads it. In initializers, whether work happens before or after delegation matters: delegating first lets downstream classes establish foundations; working first lets a layer transform arguments before they continue. Both can be valid, but mixing assumptions creates temporal coupling.

Return values need a contract too. A method that decorates super().render() assumes a downstream implementation returns renderable data. A peer that switches to mutating a buffer and returning None breaks every upstream decorator despite accepting the same arguments. Exceptions, idempotence, and sync-versus-async behavior are similarly part of cooperation.

Test the smallest combinations that production permits, including reversed independent mixin order when both orders claim support. Assert the MRO and externally meaningful effects, not private call counters alone. A test that instantiates each mixin directly misses the defining condition: its next method changes in a combined subclass.

Framework base classes deserve extra caution. Read whether their documented methods are cooperative before inserting mixins. A third-party method that intentionally terminates dispatch cannot be repaired by your upstream class. Put such a base at a documented endpoint, use an adapter, or favor composition.

super also supports maintenance. A new class inserted into the hierarchy can participate without edits to every existing caller, provided it honors the protocol. Hard-coded base calls freeze an old topology and produce duplicate or skipped work after that insertion. The flexibility is real, but it is earned through a strict shared contract rather than supplied automatically by syntax.

Exercises

  1. Add a Timed class between Audit and Validate. Make all three contribute exactly once and print the resulting MRO.
  2. Modify the cooperative initializer to reject a misspelled retry argument at the endpoint. Explain why silently ignoring extras makes APIs brittle.
  3. Construct two base orders that Python rejects and identify the contradictory precedence constraints.
  4. Rewrite a two-mixin hierarchy as explicit composition. Compare discoverability, type signatures, and configurability.
  5. Write a cooperative class method that normalizes input while preserving the most-derived class.

Keep this model

super is not a parent selector. It combines a cursor class, a bound object or subclass, and that object's MRO. Attribute lookup starts immediately after the cursor, and descriptors bind to the original object. Cooperative methods work because every participant accepts a compatible call, performs its part, and advances the same chain exactly once.

This model explains diamonds without duplicate calls, class-method factories that preserve subclasses, and failures caused by one non-delegating implementation. More importantly, it exposes the design boundary: if participants cannot share a stable protocol, composition is probably the honest structure.

Primary sources