Multiple Inheritance in Python: Complete Method Resolution Order and Super() Implementation Guide

Multiple Inheritance in python

Multiple inheritance has a bad reputation, and for a while I avoided it entirely because every tutorial I read warned me about the “diamond problem” like it was a horror story. Then I actually sat down and worked through how Python resolves it — through something called C3 linearization — and realized the language has a genuinely elegant, deterministic answer to a problem that trips up a lot of other object-oriented languages. This guide walks through how multiple inheritance actually works in Python, what MRO is, how super() cooperates with it, and when you should — and shouldn’t — reach for it.

The Diamond Problem, Concretely

Here’s the classic setup:

class Animal:
    def speak(self):
        return "..."

class Swimmer(Animal):
    def speak(self):
        return "Splash"

class Flyer(Animal):
    def speak(self):
        return "Flap"

class Duck(Swimmer, Flyer):
    pass

d = Duck()
print(d.speak())  # Splash

Duck inherits from both Swimmer and Flyer, which both inherit from Animal. When you call d.speak(), which speak runs? Python needs a consistent rule, and that rule is the Method Resolution Order (MRO).

What MRO Actually Is

Every class has an MRO — a specific, deterministic linear ordering of the class and all its ancestors, used to decide where attribute and method lookups search first. You can see it directly:

print(Duck.__mro__)
# (<class 'Duck'>, <class 'Swimmer'>, <class 'Flyer'>, <class 'Animal'>, <class 'object'>)

Or via Duck.mro(), which returns the same thing as a list. Python computes this using an algorithm called C3 linearization, introduced in Python 2.3 specifically to make multiple inheritance predictable.

How C3 Linearization Works

The C3 algorithm merges the MROs of all parent classes plus the list of parents itself, following two rules:

  1. A class always appears before its parents (local precedence order is preserved).
  2. The order parents are listed in the class definition is preserved.

For class Duck(Swimmer, Flyer), the merge looks roughly like:

L[Duck] = Duck + merge(L[Swimmer], L[Flyer], [Swimmer, Flyer])
        = Duck + merge([Swimmer, Animal, object], [Flyer, Animal, object], [Swimmer, Flyer])
        = Duck, Swimmer, Flyer, Animal, object

The algorithm repeatedly takes the head of the first list, as long as that class doesn’t appear in the tail of any other list, and appends it to the result. This guarantees monotonicity — a class never appears before a subclass, and the order you declared your bases in is respected. If C3 can’t find a consistent linearization (a genuine contradiction in your class hierarchy), Python raises TypeError: Cannot create a consistent method resolution order at class-definition time rather than silently picking an arbitrary answer.

super() Is Not “My Parent Class”

This was the biggest misconception I had to unlearn. super() doesn’t mean “call my direct parent’s method.” It means “call the next class in the MRO, from this class’s position onward.” That distinction only matters in single inheritance, but it’s everything in multiple inheritance.

class Animal:
    def speak(self):
        return "..."

class Swimmer(Animal):
    def speak(self):
        return "Splash + " + super().speak()

class Flyer(Animal):
    def speak(self):
        return "Flap + " + super().speak()

class Duck(Swimmer, Flyer):
    def speak(self):
        return "Quack + " + super().speak()

d = Duck()
print(d.speak())
# Quack + Splash + Flap + ...

Even though Swimmer only “knows about” Animal as its base class, super().speak() inside Swimmer actually calls Flyer.speak, because that’s what comes next in Duck‘s MRO — not Animal. This is the cooperative multiple inheritance pattern: every class in the chain calls super() and trusts the MRO to route the call correctly, letting all classes in the diamond participate exactly once, in a well-defined order, without any of them needing to know the full hierarchy.

Mixins: The Practical Use Case

The place multiple inheritance genuinely shines is mixins — small classes that add one specific piece of reusable behavior and are never meant to be instantiated alone.

class JSONSerializableMixin:
    def to_json(self):
        import json
        return json.dumps(self.__dict__)

class LoggingMixin:
    def log(self, message):
        print(f"[{self.__class__.__name__}] {message}")

class User(JSONSerializableMixin, LoggingMixin):
    def __init__(self, name):
        self.name = name

u = User("Ayesha")
u.log("created")          # [User] created
print(u.to_json())        # {"name": "Ayesha"}

This is how Django’s class-based views work, and it’s a pattern used extensively across the standard library too — for example, socketserver.ThreadingMixIn is combined with a server class to add threading behavior without duplicating server logic.

__init__ and Cooperative Constructors

A common trap: forgetting that constructors need to cooperate too. If each __init__ doesn’t call super().__init__(), some classes in the diamond simply never get initialized.

class Base:
    def __init__(self):
        print("Base init")
        self.base_ready = True

class A(Base):
    def __init__(self):
        super().__init__()
        print("A init")

class B(Base):
    def __init__(self):
        super().__init__()
        print("B init")

class C(A, B):
    def __init__(self):
        super().__init__()
        print("C init")

C()
# Base init
# B init
# A init
# C init

Note the order: Base runs first because it’s last in the MRO, and each __init__ calls super().__init__() before doing its own work, so the chain unwinds from the bottom of the MRO back up to C. If A or B forgot to call super().__init__(), Base.__init__ (and anything after it in the MRO) would never run, which is a very real, very common bug in multiple-inheritance codebases.

Handling __init__ Signature Mismatches

Cooperative __init__ gets harder when classes need different arguments. The standard technique is **kwargs passthrough:

class Base:
    def __init__(self, **kwargs):
        self.base_ready = True
        super().__init__(**kwargs)

class A(Base):
    def __init__(self, a_val=None, **kwargs):
        self.a_val = a_val
        super().__init__(**kwargs)

class B(Base):
    def __init__(self, b_val=None, **kwargs):
        self.b_val = b_val
        super().__init__(**kwargs)

class C(A, B):
    pass

c = C(a_val=1, b_val=2)
print(c.a_val, c.b_val)  # 1 2

Every __init__ accepts and forwards **kwargs, so extra arguments meant for classes further down the MRO pass through cleanly instead of causing a TypeError.

Inspecting and Debugging MRO

Two tools I use constantly when multiple inheritance gets complicated:

print(Duck.__mro__)              # tuple form
print(Duck.mro())                # list form
import inspect
print(inspect.getmro(Duck))      # same as __mro__, works on any object's class

When Python can’t compute a consistent order, it fails loudly at class definition:

class X: pass
class Y(X): pass
class Z(X, Y): pass
# TypeError: Cannot create a consistent method resolution order (MRO) for bases X, Y

Here Z(X, Y) demands X before Y, but Y already inherits from X, so X must come after Y — a direct contradiction, and Python refuses to guess.

Performance and Implementation Notes

MRO is computed once when the class is created (at class statement execution time), not on every attribute lookup — it’s cached on the class as __mro__. Attribute and method lookups then walk this precomputed tuple, which is why multiple inheritance doesn’t add meaningful runtime overhead compared to single inheritance for typical use — the expensive part (linearization) happens once, not per call.

Common Mistakes

  • Treating super() as “call the parent.” It calls the next class in MRO order, which depends on the full class hierarchy, not just direct ancestry.
  • Forgetting super().__init__() in one branch of a diamond. This silently skips initialization for classes further along the MRO.
  • Deep, tangled mixin hierarchies “just because you can.” Multiple inheritance is best kept shallow and intentional — mixins that each do one clearly named thing — not a substitute for composition when composition would be clearer.
  • Ignoring MRO conflicts and just reordering base classes randomly until it works. Understand why the order matters instead of guessing.

FAQs

Q: Is multiple inheritance considered bad practice in Python? Not inherently — Python’s deterministic MRO makes it far safer than in languages without a formal resolution algorithm. It’s genuinely useful for mixins. It becomes a problem when hierarchies get deep and tangled for no real benefit; composition is often clearer for complex behavior combinations.

Q: What’s the difference between MRO and simple depth-first search? Old-style classes in Python 2 used a naive depth-first left-to-right search, which could violate monotonicity and produce surprising results in diamond hierarchies. C3 linearization (used by all new-style classes, which is all classes in Python 3) fixes this by guaranteeing consistency.

Q: Can I call a specific parent’s method instead of following MRO? Yes — you can call Flyer.speak(self) directly instead of super().speak(), bypassing MRO. This is occasionally necessary but breaks cooperative inheritance, so use it deliberately and sparingly.

Summary

Multiple inheritance in Python is governed by a precise, deterministic algorithm — C3 linearization — that produces a class’s MRO: a fixed order in which ancestors are searched for attributes and methods. super() follows this order rather than jumping straight to a “parent,” which is what makes cooperative multiple inheritance (used heavily in mixins) actually work correctly. Used deliberately — small, single-purpose mixins, consistent super().__init__() calls, **kwargs passthrough — multiple inheritance is a clean, well-defined tool rather than the minefield its reputation suggests.

References

Total
0
Shares

Leave a Reply

Previous Post
Monkey Patching in python

Monkey Patching in Python: Complete Runtime Modification and Dynamic Programming Techniques Guide

Next Post
Default values for instance variables in python

Default Values for Instance Variables in Python: Complete Class Initialization and Attribute Management Guide

Related Posts