Multiple inheritance lets one class inherit from more than one parent, and when it does, Python needs a rule for deciding which version of a method to run when two parents define the same one. That rule is the method resolution order — the MRO — and you can see it directly on any class.
Multiple inheritance is Python's most powerful and most misused feature. It is how the standard library implements mixins like socketserver.ThreadingMixIn, and how frameworks add optional capabilities to a base class without touching it. It is also how you end up with a class whose behaviour depends on the order you wrote the parents in.
How do you see the method resolution order?
Every class has a __mro__ attribute holding a tuple of itself and all its ancestors, in the order Python searches them. It starts with the class itself and always ends with object.
class Base:
pass
class Left(Base):
pass
class Right(Base):
pass
class Child(Left, Right):
pass
for cls in (Child, Left, Base):
print(cls.__name__, "->", [c.__name__ for c in cls.__mro__])
Reading that tuple from left to right gives you the search order: Child first, then Left, then Right, then Base, then object. When you call a method that is not defined on Child, Python looks in Left first. Only if Left does not define it does it move on to Right.
The super() function from the previous lesson is the mechanism that walks this list. Each super() call moves one position to the right, which is why a chain of super() calls visits every class in the MRO exactly once.
What happens when two parents define the same method?
The one that appears first in the MRO wins — which is the one you listed first in the class statement. Python does not merge the two implementations; the second one is simply not called unless the first explicitly delegates to it with super().
class LoggingMixin:
def handle(self, item):
return f"logged {item}"
class RetryingMixin:
def handle(self, item):
return f"retried {item}"
class Worker(LoggingMixin, RetryingMixin):
def handle(self, item):
return super().handle(item)
class OtherWorker(RetryingMixin, LoggingMixin):
def handle(self, item):
return super().handle(item)
print("logging first :", Worker().handle("job"))
print("retrying first:", OtherWorker().handle("job"))
That is the practical meaning of "the MRO decides". Write class Worker(LoggingMixin, RetryingMixin) and you get logging first; swap the order and you get retrying first. The order of the parent list is a real design decision with observable consequences.
How do multiple parents cooperate?
The pattern that makes multiple inheritance manageable is called cooperative multiple inheritance: every mixin's method delegates to super() and forwards what comes back, so the chain composes all the behaviours instead of stopping at the first one.
class BaseProcessor:
def process(self, item):
return f"processed {item}"
class RetryMixin:
def process(self, item):
return f"retried({super().process(item)})"
class TimingMixin:
def process(self, item):
return f"timed({super().process(item)})"
class LoggingMixin:
def process(self, item):
return f"logged({super().process(item)})"
class Pipeline(LoggingMixin, TimingMixin, RetryMixin, BaseProcessor):
def process(self, item):
return super().process(item)
print(Pipeline().process("job-1"))
print([c.__name__ for c in Pipeline.__mro__])
Compare the output with the previous example. Here all three behaviours appear, because each class hands off to the next. If any one of them omitted the super() call, everything after it in the chain would be skipped and the output would quietly lose a step.
This is why the standard library's mixins are written this way. ThreadingMixIn does not know what class it will be mixed into; it just knows it should do its work and then let the next class in the MRO continue.
What is the diamond problem?
When a class inherits from two parents that share a common ancestor, the MRO looks like a diamond. The classic diagram has a D at the bottom, B and C in the middle and A at the top, with A(D) inheriting from both B and C.
class D:
def handle(self, item):
return f"D handled {item}"
class B(D):
def handle(self, item):
return "B -> " + super().handle(item)
class C(D):
def handle(self, item):
return "C -> " + super().handle(item)
class A(B, C):
def handle(self, item):
return "A -> " + super().handle(item)
print(A().handle("x"))
print("D appears once in the MRO:", [c.__name__ for c in A.__mro__].count("D"))
Python's MRO solves the ordering half of this — D appears exactly once in the tuple, at the end — so the cooperative chain does not visit it twice. What MRO does not solve on its own is the behaviour: if B and C both call super() and D's method has a side effect such as printing, that side effect still happens once, but it happens at the point where the chain reaches D rather than at the top.
Practical advice: keep shared ancestors shallow and their behaviour free of side effects. A mixin that only adds a method and forwards everything upwards is safe. A base class with mutable state inherited by two branches is where the confusion starts.
When should you actually use multiple inheritance?
Mostly for mixins: small, focused classes that add one capability and are combined at the end. class MyHandler(LoggingMixin, TimeoutMixin, BaseHandler) reads clearly and each mixin stays independently testable.
Avoid it when you are tempted to use it to avoid duplicating a method. If two classes need the same behaviour, the answer is usually a shared base class or a module-level function, not a second parent.
| What you want to express | How to express it |
|---|---|
| a shared capability with no state | a mixin, combined last in the parent list |
| a contract several classes must satisfy | an abstract base class with @abstractmethod |
| shared implementation code | a base class, or a module-level helper |
| an optional second implementation | composition with an injected collaborator |
Also avoid it when the parents are unrelated domains — inheriting from both File and Connection because you want both sets of methods produces a class with no coherent meaning. Composition handles that case better, and the last lesson in this module covers it in full.
In summary
Cls.__mro__shows the search order: the class itself first,objectlast.- When two parents define the same method, the one earlier in the MRO wins.
- Cooperative mixins call
super()and forward the result, so all behaviours compose. - Omitting a
super()call silently drops everything later in the chain. - Use multiple inheritance for focused mixins, not to dodge duplication.
Try it yourself: build a mixin stack
Write three mixins with no state of their own: LoggingMixin with a process(item) method, TimingMixin with a process(item) method, and RetryMixin with a process(item) method. Each one calls super().process(item) and wraps the result.
Combine them as class Pipeline(LoggingMixin, TimingMixin, RetryMixin): and check that the output contains all three contributions in the right order. Now swap two parents and confirm the output order changes while all three contributions remain. Finally, print Pipeline.__mro__ and Pipeline(LoggingMixin, TimingMixin, RetryMixin).__mro__ and account for every name in the tuple.