Cooperative Multiple Inheritance — Core Concepts

The Central Idea

Python allows a class to inherit from multiple parents. When those parents define the same method (say, __init__), Python needs a way to call all of them in a predictable order. The solution is cooperative multiple inheritance: every class in the chain calls super(), which forwards the call to the next class in the Method Resolution Order (MRO).

How super() Actually Works

super() doesn’t just mean “call my parent.” It means “call the next class in the MRO.” This is a crucial distinction.

Consider classes A, B, and C, where C inherits from both A and B. When C calls super().__init__(), it calls A.__init__(). When A calls super().__init__(), it calls B.__init__() — even though B is not a parent of A. The MRO determines the full chain.

The MRO Chain

Python computes the MRO using the C3 linearization algorithm. You can inspect it with ClassName.__mro__ or ClassName.mro().

For class C(A, B), the MRO is typically: C → A → B → object.

This means:

  1. C.__init__ runs first.
  2. super() in C calls A.__init__.
  3. super() in A calls B.__init__.
  4. super() in B calls object.__init__.

Every class gets its turn.

The Cooperation Contract

For this to work, every class must follow two rules:

  1. Always call super() in the method you’re overriding.
  2. Accept **kwargs and pass unrecognized arguments through — because different classes in the chain may expect different parameters.

Breaking either rule breaks the chain. If A doesn’t call super().__init__(), then B.__init__ never runs, and B’s attributes are never set.

Where This Matters

Mixin Classes

Mixins are the most common use case. A mixin adds one specific behavior (logging, serialization, caching) and cooperates with whatever other classes are in the chain:

  • class LoggingMixin — adds logging to any class
  • class TimestampMixin — adds created/updated timestamps
  • class SerializableMixin — adds to_dict() capability

Framework Base Classes

Django uses cooperative inheritance extensively. View, TemplateView, FormView, and mixins like LoginRequiredMixin all cooperate through super() calls.

Common Misconception

“super() calls the parent class.” Not exactly. super() calls the next class in the MRO, which might not be a direct parent. This is what makes cooperative inheritance work across complex hierarchies, but it’s also what trips people up when they assume direct parent-child relationships.

The **kwargs Pattern

When classes in the chain expect different constructor arguments, use **kwargs forwarding:

class A:
    def __init__(self, x, **kwargs):
        super().__init__(**kwargs)
        self.x = x

class B:
    def __init__(self, y, **kwargs):
        super().__init__(**kwargs)
        self.y = y

class C(A, B):
    def __init__(self, x, y, z, **kwargs):
        super().__init__(x=x, y=y, **kwargs)
        self.z = z

Each class picks off the arguments it knows about and forwards the rest.

When It Goes Wrong

SymptomCause
TypeError: __init__() got unexpected keyword argumentA class in the chain doesn’t accept **kwargs
Attribute missing at runtimeA class forgot to call super().__init__()
Method called in wrong orderMRO not what you expected — check ClassName.__mro__
TypeError: Cannot create a consistent MROContradictory inheritance hierarchy — C3 linearization fails

Composition vs. Cooperative Inheritance

Not every “has multiple abilities” situation needs multiple inheritance. If the behaviors don’t need to share the same method chain, composition (holding references to helper objects) is simpler and less fragile. Use cooperative inheritance when behaviors genuinely need to layer on top of each other in a method chain.

One thing to remember: Cooperative multiple inheritance works through a contract — every class calls super() and passes along arguments it doesn’t recognize. Break the contract in one place, and the whole chain breaks.

pythonadvancedoop

See Also