Skillversitas

Multiple Inheritance and MRO

Python allows a class to list several bases, and it resolves the resulting diamond problem with the C3 linearization - the Method Resolution Order, or MRO. The MRO is a single, consistent order in which Python searches for a name, computed when the class is created and inspectable on every class. Multiple inheritance in Python is usually a tool for mixins - small classes that add a behavior without being a full kind of thing - and the MRO is what makes the pattern safe. This chapter computes MROs by hand, walks the diamond, and shows the mixin idiom that is the library's legitimate use of the feature.

Reading the MRO

class A:
    def hello(self):
        return "A"

class B(A):
    pass

class C(A):
    def hello(self):
        return "C"

class D(B, C):
    pass

print(D.mro())
# [D, B, C, A, object] - the exact search order
print(D.__mro__)          # same tuple
D().hello()               # 'C'? No - B is searched first: 'A'... see below

D.mro() is the definitive answer to "which class's method runs": Python walks that list and uses the first match. The C3 rule that produces it has three properties: a class comes before its bases, the bases' relative order from the class statement is preserved where possible, and there is exactly one consistent order - if no order satisfies all three, Python raises TypeError: Cannot create a consistent MRO at class-creation time, which is the language telling you the bases conflict. The MRO is not a hidden detail; it is a first-class, inspectable object, and printing it is the first debugging move in any inheritance surprise.

The Diamond, Resolved

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

class Left(Base):
    def speak(self):
        return "left"

class Right(Base):
    def speak(self):
        return "right"

class Both(Left, Right):
    pass

print(Both.mro())        # [Both, Left, Right, Base, object]
print(Both().speak())    # 'left' - Left precedes Right in the statement

# the diamond is visited ONCE: Base is not duplicated
# Left and Right both inherit Base; the MRO keeps a single Base slot

The diamond (Both -> Left, Right -> Base) is the case that breaks naive "leftmost depth-first" schemes, which would visit Base twice. C3 linearization guarantees a single, coherent order: Both, Left, Right, Base, object - the class, then its bases in statement order, then their bases in a conflict-free merge, with each class appearing exactly once. The consequence is predictable: Both().speak() is Left.speak because Left comes first in the statement, and if Left.speak calls super().speak(), it lands on Right, then Base - the same order the MRO shows. When two bases define conflicting orders (e.g. class X(C, B) vs class Y(B, C) combined in a child), no order exists and the class definition itself fails - fail early, at definition, not at call time.

Mixins: The Legitimate Use

class Logged:
    """Mixin: records calls to any method named 'action'."""
    def __init_subclass__(cls, **kw):
        super().__init_subclass__(**kw)

    def log(self, msg):
        self._log = getattr(self, "_log", [])
        self._log.append(msg)
        return msg

class Timed:
    """Mixin: adds a monotonic clock to the host class."""
    import time as _time
    def now(self):
        return self._time.monotonic()

class Sensor(Logged, Timed):
    def __init__(self, name):
        self.name = name

    def action(self, value):
        return self.log(f"{self.name}={value}")

s = Sensor("temp")
print(s.action(21.5))     # 'temp=21.5'
print(s.now() >= 0)       # True - Timed's method, on a Sensor

A mixin is a class that provides methods but is not meant to be instantiated on its own - it adds a capability (Logged adds logging, Timed adds a clock) to whatever combines it. The conventions that keep mixins sane: name them for the capability (Logged, Serializable, Iterable), give them no state of their own (or only state the host class expects), give them no conflicting __init__ (the host owns initialization), and list them after the primary base (class Sensor(Logged, Timed) or class Sensor(Timed, Logged) - the primary kind first, mixins next, in the order their capabilities should resolve). The standard library is full of them: abc.ABC, typing mixins, the *MixIn names in older code.

The super() Chain Across Multiple Bases

class A:
    def step(self):
        print("A")

class B(A):
    def step(self):
        print("B")
        super().step()      # continues the chain

class C(A):
    def step(self):
        print("C")
        super().step()

class D(B, C):
    def step(self):
        print("D")
        super().step()

D().step()
# D
# B
# C
# A
# - each super() follows the MRO, not "my direct parent"

In multiple inheritance, super() does not mean "call my parent" - it means "continue the MRO from after this class." In D(B, C), super().step() from D goes to B; from B it goes to C (not back to A, because C is next in the MRO [D, B, C, A, object]); from C it reaches A. This is the cooperative multiple-inheritance pattern: every override that participates in the chain calls super() exactly once, and the MRO guarantees every class in the order gets its turn exactly once. It is elegant and it is fragile - one base that forgets the super() call silently short-circuits the chain, which is why the single-responsibility mixin (one capability, one method, always calls super) is the safe shape.

When NOT to Use It

  • Two full "kinds" with state. A class that is both a ListModel and a TreeModel with its own storage for each is a design smell - compose instead.
  • Conflicting __init__s. If the bases' constructors disagree about what the object is, the MRO cannot mediate; pick one primary base.
  • Inherited state, not behavior. Multiple inheritance for sharing a few attributes is usually simpler as a mixin that provides methods, or as plain composition.

Summary

Python resolves multiple inheritance with the C3 linearization - the MRO, a single conflict-free search order you can read via Class.mro() and that makes the diamond visit each base exactly once. The idiomatic use is mixins: small stateless capability classes (Logged, Timed) listed after the primary base, whose methods cooperate through super() - which follows the MRO, not the direct parent, so a cooperative chain visits every class in order exactly once. Avoid multiple inheritance for two full kinds with their own state or conflicting constructors - compose instead - and when the bases' orders genuinely conflict, Python fails the class definition at creation time, which is the safest possible moment to find out.