Python Multiple Inheritance
Python is one of the few mainstream languages that lets a class inherit directly from more than one parent class at once — a feature called multiple inheritance. Instead of building a single rigid chain of parent-to-child classes, you can combine independent pieces of behavior (often called mixins) into one class. This is powerful, but it also introduces real complexity: Python has to decide, in a well-defined order, which parent’s method or attribute wins when more than one parent defines the same name. Understanding that ordering — the Method Resolution Order (MRO) — is the key to using multiple inheritance safely.
Overview / How it works
Single inheritance connects a child class to exactly one parent, forming a straight line. Multiple inheritance lets a class list several parents inside the parentheses of the class statement, for example class Child(ParentA, ParentB):. The child inherits attributes and methods from both parents and can override either of them.
Internally, every class has a Method Resolution Order (MRO) — a fixed, linear sequence of classes that Python consults whenever it looks up an attribute or method. When you write obj.method(), the interpreter does not vaguely search “the parent”; it walks type(obj).__mro__ from left to right and uses the first matching definition it finds. You can inspect this order directly: every class exposes it as ClassName.__mro__ (a tuple) or ClassName.mro() (a list).
Python computes the MRO using an algorithm called C3 linearization. It guarantees three things: a subclass always appears before its parents, the left-to-right order you listed the base classes in is preserved, and these guarantees hold consistently for every class in the hierarchy at once. If they cannot all be satisfied — typically because two base classes disagree about the relative order of a shared ancestor — Python refuses to create the class and raises a TypeError at class-definition time. This is usually called an “MRO conflict”; see Common Mistakes below.
The classic scenario that makes multiple inheritance tricky is the diamond problem: classes B and C both inherit from the same base A, and a class D inherits from both B and C. Without a well-defined lookup order, it would be ambiguous whether D sees A‘s behavior through B or through C, and whether A‘s initializer runs once or twice. The MRO answers the ordering question deterministically, and super() used cooperatively — as shown in Example 2 — answers the “runs once” question.
Syntax
class ChildClass(BaseClass1, BaseClass2, ..., BaseClassN):
def __init__(self, ...):
super().__init__(...)
# additional attributes and methods
BaseClass1, BaseClass2, ...— one or more existing classes, listed left to right in the order that determines lookup priority.super()— inside a method, returns a proxy that forwards lookups to the next class in the current instance’s MRO, which is not alwaysBaseClass1.ChildClass.__mro__— a tuple showing the full resolution order, always ending inobject.ChildClass.mro()— the same information as a list; handy for debugging.
Examples
Example 1: Combining two unrelated capabilities
class Swimmer:
def swim(self):
return "swimming"
class Flyer:
def fly(self):
return "flying"
class Duck(Swimmer, Flyer):
def __init__(self, name):
self.name = name
donald = Duck("Donald")
print(donald.swim())
print(donald.fly())
print(Duck.__mro__)
Output:
swimming
flying
(<class '__main__.Duck'>, <class '__main__.Swimmer'>, <class '__main__.Flyer'>, <class 'object'>)
Duck has no common ancestor conflict here — Swimmer and Flyer are independent classes, so this is the simplest possible case: Duck simply gains both sets of methods, and the printed __mro__ shows the straightforward left-to-right search order Python will use.
Example 2: The diamond problem and cooperative super()
class Animal:
def __init__(self, name):
self.name = name
print(f"Animal.__init__ for {name}")
class Walker(Animal):
def __init__(self, name, **kwargs):
super().__init__(name, **kwargs)
self.can_walk = True
print("Walker.__init__")
class Swimmer(Animal):
def __init__(self, name, **kwargs):
super().__init__(name, **kwargs)
self.can_swim = True
print("Swimmer.__init__")
class Duck(Walker, Swimmer):
def __init__(self, name):
super().__init__(name)
print("Duck.__init__")
d = Duck("Donald")
print(Duck.__mro__)
print(d.can_walk, d.can_swim)
Output:
Animal.__init__ for Donald
Swimmer.__init__
Walker.__init__
Duck.__init__
(<class '__main__.Duck'>, <class '__main__.Walker'>, <class '__main__.Swimmer'>, <class '__main__.Animal'>, <class 'object'>)
True True
This is the diamond shape: Walker and Swimmer both inherit from Animal, and Duck inherits from both. Because every __init__ calls super().__init__(...) instead of calling a specific parent by name, each class in the MRO — Duck → Walker → Swimmer → Animal — runs exactly once, and Animal.__init__ is not run twice. Notice the order of the printed messages follows the MRO from the innermost super() call outward: Animal finishes first (it’s at the end of the chain), and each caller prints its own message only after its super().__init__() call returns.
Example 3: A realistic mixin combination
import json
class JSONSerializableMixin:
def to_json(self) -> str:
return json.dumps(self.__dict__)
class LoggingMixin:
def log(self, message: str) -> None:
print(f"[{self.__class__.__name__}] {message}")
class Product(JSONSerializableMixin, LoggingMixin):
def __init__(self, name: str, price: float):
self.name = name
self.price = price
def apply_discount(self, percent: float) -> None:
self.price = round(self.price * (1 - percent / 100), 2)
self.log(f"Applied {percent}% discount, new price is {self.price}")
widget = Product("Widget", 19.99)
widget.apply_discount(10)
print(widget.to_json())
print(isinstance(widget, JSONSerializableMixin))
print(isinstance(widget, LoggingMixin))
Output:
[Product] Applied 10% discount, new price is 17.99
{"name": "Widget", "price": 17.99}
True
True
This is the pattern multiple inheritance is best at in real code: small, focused mixin classes that each add one capability (serialization, logging) without knowing anything about each other. Product combines them and gets both behaviors for free, and isinstance checks confirm the mixins are genuinely part of Product‘s type hierarchy.
Under the hood: how the MRO is built
When Python parses class Duck(Walker, Swimmer):, it does not wait until you call a method to figure out the search order — it computes the full MRO immediately, using C3 linearization, and stores it as Duck.__mro__. Conceptually, the algorithm merges several lists: the MRO of the first base, the MRO of the second base, and so on, followed by the literal list of bases itself. It repeatedly takes the head of the first list that does not appear in the tail of any other list, appends it to the result, and removes it everywhere. This is what enforces “subclasses before parents” and “preserve the order you wrote the bases in” simultaneously. If no valid head can ever be found, the merge is impossible and Python raises the MRO TypeError instead of guessing.
Once the MRO exists, two different lookup mechanisms use it:
- Plain attribute access (
obj.attr) walkstype(obj).__mro__from the start and returns the first match — instance__dict__is checked first for data attributes, then the class MRO for everything else. super()does not mean “my direct parent.” Called with no arguments inside a method, it is shorthand forsuper(CurrentClass, self), which findsCurrentClassintype(self).__mro__and returns a proxy that starts searching one position after it. Becausetype(self).__mro__depends on the actual instance’s class, not on where the method is textually defined, the samesuper().__init__()call inWalkercan forward toSwimmerin one hierarchy and toobjectin another — that dynamic behavior is exactly what makes cooperative multiple inheritance work in Example 2.
Common Mistakes
Mistake 1: An MRO that cannot be linearized
Listing base classes in an order that contradicts an existing inheritance relationship is a class-definition-time error, not something you can catch by testing later:
class A:
pass
class B(A):
pass
class C(A, B):
pass
This raises TypeError: Cannot create a consistent method resolution order (MRO) for bases A, B. C(A, B) asks Python to place A before B (because you listed it first), but B is itself a subclass of A, so C3 also requires B to come before A. Both constraints can’t hold, so Python refuses to build the class. The fix is simply to list the more specific class first: class C(B, A): pass works, because B‘s own MRO already places it ahead of A.
Mistake 2: Calling parent __init__ methods directly instead of using super()
It’s tempting to call each base class’s __init__ by name, but in a diamond hierarchy this runs the shared ancestor’s initializer more than once:
class Base:
def __init__(self):
print("Base.__init__")
class Left(Base):
def __init__(self):
Base.__init__(self)
print("Left.__init__")
class Right(Base):
def __init__(self):
Base.__init__(self)
print("Right.__init__")
class Child(Left, Right):
def __init__(self):
Left.__init__(self)
Right.__init__(self)
print("Child.__init__")
Child()
Output:
Base.__init__
Left.__init__
Base.__init__
Right.__init__
Child.__init__
Base.__init__ runs twice — once through Left, once through Right — which can silently duplicate side effects like opening a file handle or appending to a shared list. The fix is to make every class in the hierarchy call super().__init__() instead of naming a specific parent, so the MRO controls the order and each class runs exactly once:
class Base:
def __init__(self):
print("Base.__init__")
class Left(Base):
def __init__(self):
super().__init__()
print("Left.__init__")
class Right(Base):
def __init__(self):
super().__init__()
print("Right.__init__")
class Child(Left, Right):
def __init__(self):
super().__init__()
print("Child.__init__")
Child()
Output:
Base.__init__
Right.__init__
Left.__init__
Child.__init__
Base.__init__ now runs a single time, at the point dictated by Child‘s MRO (Child → Left → Right → Base → object).
Best Practices
- Keep multiple-inheritance hierarchies shallow and prefer small, single-purpose mixin classes over deep webs of “real” base classes.
- Always call
super().__init__(*args, **kwargs)— not a hardcoded parent name — so every class in the chain cooperates correctly regardless of what it’s mixed with. - Accept and forward
**kwargsin mixin__init__methods so classes further down the MRO still receive the arguments meant for them. - Name mixins with a
Mixinsuffix (e.g.LoggingMixin) so their role is obvious at a glance, and pick a bases order deliberately — check__mro__if you’re unsure which method wins. - Avoid giving two parent classes the same attribute or method name unless you intend for one to deliberately shadow the other; if you do, know which one wins by checking
__mro__. - When in doubt, print
ClassName.__mro__(or runhelp(ClassName)) while debugging — it removes all guesswork about lookup order. - Consider whether composition (storing a helper object as an attribute) is simpler than inheritance before reaching for a second base class — multiple inheritance is a tool for genuinely “is-a” relationships, not just code reuse.
Practice Exercises
- Define a
Flyablemixin with afly()method and aSwimmablemixin with aswim()method. Create aDuckclass using both, then printDuck.__mro__and confirm the order matches what you expect. - Build a small diamond hierarchy of your own (a
Vehiclebase,ElectricandMotorizedsubclasses, and aHybridCarthat inherits from both). Give each__init__a cooperativesuper().__init__(**kwargs)call and verify the base class initializer runs exactly once. - Write two classes,
AandB(A), then try to defineclass C(A, B): pass. Predict the error before running it, then explain in your own words why the MRO cannot be built.
Summary
- Multiple inheritance lets a class list more than one base class:
class Child(ParentA, ParentB):. - Python resolves attribute and method lookups using a per-class Method Resolution Order (MRO), computed with the C3 linearization algorithm and inspectable via
__mro__ormro(). - The C3 algorithm guarantees subclasses precede their parents and preserves the order bases were listed in; when that’s impossible, Python raises a
TypeErrorat class-definition time. super()forwards to the next class in the MRO, not to a hardcoded parent — this is what lets diamond hierarchies initialize each ancestor exactly once when every class callssuper().__init__()cooperatively.- Calling a specific parent’s method by name instead of using
super()is the most common source of duplicated initialization bugs in multiple-inheritance code. - Favor small, focused mixin classes and shallow hierarchies; reach for composition when inheritance isn’t a genuine “is-a” relationship.
