Cooperative Multiple Inheritance — Deep Dive

Why Cooperative Inheritance Exists

Languages like Java avoid multiple inheritance entirely (using interfaces instead). C++ allows it but doesn’t enforce cooperation, leading to the “deadly diamond of death.” Python took a middle path: allow multiple inheritance, but provide super() as a protocol for cooperative method dispatch.

The result is a powerful system that enables mixin patterns, framework extensibility, and aspect-oriented-style programming — but it requires every participant to follow the rules.

C3 Linearization: How the MRO Is Computed

Python’s MRO uses the C3 linearization algorithm (introduced in Python 2.3). It guarantees two properties:

  1. Local precedence order: If a class lists parents as (A, B), then A comes before B in the MRO.
  2. Monotonicity: If A comes before B in a parent’s MRO, A comes before B in the child’s MRO too.

The Algorithm

For a class C(B1, B2, ..., Bn):

L[C] = C + merge(L[B1], L[B2], ..., L[Bn], [B1, B2, ..., Bn])

The merge operation repeatedly:

  1. Takes the first element of the first list.
  2. Checks that this element doesn’t appear in the tail of any other list.
  3. If clean, adds it to the result and removes it from all lists.
  4. If not clean, moves to the next list.

If no list offers a clean head, C3 fails and Python raises TypeError: Cannot create a consistent method resolution order.

Practical Example

class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass

print(D.__mro__)
# (D, B, C, A, object)

The linearization ensures B comes before C (because D lists B first), and both come before A (because both inherit from A).

The super() Protocol in Detail

super() is not syntactic sugar for “call parent.” It’s a descriptor that:

  1. Inspects the MRO of the instance’s class.
  2. Finds the current class in the MRO.
  3. Returns a proxy that delegates to the next class in the MRO.
class A:
    def method(self):
        print("A.method")
        super().method()  # Calls next in MRO, which might be B, not object

class B:
    def method(self):
        print("B.method")
        super().method()  # Calls object.method (if it exists)

class C(A, B):
    def method(self):
        print("C.method")
        super().method()  # Calls A.method

c = C()
c.method()
# Output:
# C.method
# A.method
# B.method

Notice: A.method() calls B.method() via super(), even though A doesn’t inherit from B. This is cooperative dispatch — the MRO of C determines the chain.

Production Pattern: Robust Mixin Design

Well-designed mixins follow strict rules for cooperation:

class LoggingMixin:
    """Mixin that logs method calls. Cooperates via super()."""

    def save(self, **kwargs):
        print(f"LOG: Saving {self.__class__.__name__}")
        result = super().save(**kwargs)
        print(f"LOG: Saved {self.__class__.__name__}")
        return result


class ValidationMixin:
    """Mixin that validates before saving. Cooperates via super()."""

    def save(self, **kwargs):
        self.validate()
        return super().save(**kwargs)

    def validate(self):
        for field, rules in getattr(self, '_validators', {}).items():
            value = getattr(self, field, None)
            for rule in rules:
                rule(value)


class BaseModel:
    """Concrete base. Terminal in the super() chain."""

    def save(self, **kwargs):
        print(f"SAVE: {self.__class__.__name__} to database")
        return True


class User(LoggingMixin, ValidationMixin, BaseModel):
    _validators = {
        'email': [lambda v: None if '@' in str(v) else (_ for _ in ()).throw(ValueError("Invalid email"))]
    }

    def __init__(self, email):
        self.email = email

Call order for User().save():

  1. LoggingMixin.save — logs entry
  2. ValidationMixin.save — validates fields
  3. BaseModel.save — persists to database
  4. Back up the chain for logging exit

Constructor Cooperation: The **kwargs Protocol

The hardest part of cooperative inheritance is constructors, because different classes need different arguments:

class Identifiable:
    def __init__(self, *, id: str, **kwargs):
        super().__init__(**kwargs)
        self.id = id


class Timestamped:
    def __init__(self, *, created_at: str = "", **kwargs):
        super().__init__(**kwargs)
        self.created_at = created_at or "now"


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


class Entity(Identifiable, Timestamped, Named):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)


# All keyword arguments flow through the chain
e = Entity(id="abc", name="Widget", created_at="2026-01-01")
print(e.id, e.name, e.created_at)  # abc Widget 2026-01-01

Rules for safe constructors:

  1. Use keyword-only arguments (* in the signature).
  2. Always include **kwargs and pass them to super().__init__(**kwargs).
  3. Never assume you know every argument — other classes in the chain may need their own.

Django’s Cooperative Views

Django’s class-based views are a textbook example of cooperative inheritance:

from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView

class ProtectedArticleList(LoginRequiredMixin, ListView):
    model = Article
    template_name = "articles/list.html"
    login_url = "/login/"

The MRO is: ProtectedArticleList → LoginRequiredMixin → ListView → ... → View → object.

When a request arrives, dispatch() flows through the chain:

  1. LoginRequiredMixin.dispatch checks authentication.
  2. If authenticated, super().dispatch() reaches ListView.dispatch.
  3. ListView.dispatch routes to get() or post().

Debugging MRO Issues

Inspect the Chain

for cls in MyClass.__mro__:
    if hasattr(cls, 'method') and 'method' in cls.__dict__:
        print(f"{cls.__name__} defines method()")

Common Failures

Missing super() call:

class BrokenMixin:
    def save(self):
        print("BrokenMixin save")
        # Forgot super().save() — chain stops here

Non-cooperative third-party class: If a library class doesn’t call super(), you can’t use it in a cooperative chain. Wrap it in an adapter instead.

Inconsistent MRO:

class A: pass
class B(A): pass
class C(A, B): pass  # TypeError — A before B contradicts B(A)

Fix: class C(B, A) — let the more specific class come first.

Performance

Cooperative inheritance has no meaningful runtime cost beyond regular method calls. The MRO is computed once at class definition time and cached. super() creates a lightweight proxy object — CPython optimizes this heavily (it’s a single C-level operation in most cases).

The real cost is cognitive: developers need to understand the MRO and cooperation contract. For teams unfamiliar with the pattern, composition may be more maintainable.

When to Prefer Composition

Use cooperative inheritance when:

  • Behaviors must intercept/wrap the same method chain (like Django views).
  • You need aspect-oriented patterns (logging, validation, caching wrapping the same operation).
  • A framework mandates it (Django, SQLAlchemy).

Use composition when:

  • Behaviors are independent (a User has an Address, not “is an Address”).
  • You need to swap implementations at runtime.
  • The inheritance hierarchy would exceed 3-4 levels.

One thing to remember: Cooperative multiple inheritance is a protocol, not a language feature. super() provides the mechanism, but every class must uphold the contract — calling super() and forwarding unknown arguments. When everyone cooperates, you get elegant layered behavior. When one class defects, the chain breaks silently.

pythonadvancedoop

See Also