Multiple Inheritance and the MRO
A class can inherit from several parents. The method resolution order decides which version wins, and C3 linearisation is the algorithm that computes it.
- Basics
- Data Types
- Operators
- Strings
- Control Flow
- Lists
- Tuples
- Sets
- Dictionaries
- Comprehensions
- Functions
- Advanced Functions
- Recursion
- Exception Handling
- File Handling
- Modules
- Standard Library
- OOP
- Advanced OOP
- Iterators and Generators
- Decorators
- Context Managers
- Descriptors and Dataclasses
- Python Internals
- Concurrency
- Regular Expressions
- Serialization
- Command Line Python
- Testing and Debugging
- Type Hints
- Performance
- Python Security
- DSA with Python
Two parents
class Swimmer:
def move(self):
return "swimming"
def breathe(self):
return "gills"
class Walker:
def move(self):
return "walking"
def legs(self):
return 4
class Amphibian(Swimmer, Walker):
pass
a = Amphibian()
print(a.move()) # swimming - Swimmer is listed first
print(a.breathe()) # gills
print(a.legs()) # 4 - inherited from WalkerThe class gained everything from both parents. Where both define the same name, the leftmost parent wins.
The method resolution order
print([cls.__name__ for cls in Amphibian.__mro__])
# ['Amphibian', 'Swimmer', 'Walker', 'object']
Amphibian.mro() # the same thing as a method
help(Amphibian) # shows the MRO tooThe MRO is a single ordered list of classes. Every attribute lookup walks it from left to right and stops at the first match. There is no ambiguity at runtime, ever - the order was decided when the class was created.
The diamond
A
/ \
B C
\ /
Dclass A:
def hello(self):
return "A"
class B(A):
def hello(self):
return "B -> " + super().hello()
class C(A):
def hello(self):
return "C -> " + super().hello()
class D(B, C):
def hello(self):
return "D -> " + super().hello()
print(D().hello()) # D -> B -> C -> A
print([cls.__name__ for cls in D.__mro__]) # ['D', 'B', 'C', 'A', 'object']Look at what happened insideB.hello. Itssuper()called C, notA, even thoughCis notB's parent.super()means "the next class in the MRO of the actual object", not "my parent". This is the single most important fact about multiple inheritance in Python.
Note also that A.hello ran exactly once. A naive depth first search would have visited it twice.
C3 linearisation
The MRO is computed by an algorithm called C3 linearisation. Three rules define it:
- A class always comes before all of its parents.
- Parents keep the left to right order they were declared in.
- Each class appears exactly once.
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
class E(C, B): pass # the parents in the other order
print([c.__name__ for c in D.__mro__]) # ['D', 'B', 'C', 'A', 'object']
print([c.__name__ for c in E.__mro__]) # ['E', 'C', 'B', 'A', 'object']When no order is possible
class A: pass
class B(A): pass
# class Broken(A, B): pass
# TypeError: Cannot create a consistent method resolution order (MRO)
# for bases A, BRule 1 requires B before A; rule 2 requires A before B. No ordering satisfies both, so Python refuses to create the class. The error arrives at class definition time, not later.
Cooperative inheritance
class Base:
def __init__(self, **kwargs):
self.log = []
super().__init__(**kwargs) # pass anything left along
class Timestamped(Base):
def __init__(self, created_at=None, **kwargs):
super().__init__(**kwargs)
from datetime import date
self.created_at = created_at or date.today()
class Tagged(Base):
def __init__(self, tags=None, **kwargs):
super().__init__(**kwargs)
self.tags = tags or []
class Note(Timestamped, Tagged):
def __init__(self, text, **kwargs):
super().__init__(**kwargs)
self.text = text
def __repr__(self):
return f"Note({self.text!r}, tags={self.tags}, created={self.created_at})"
n = Note("first note", tags=["draft"])
print(n)
print([c.__name__ for c in Note.__mro__])Every __init__ in the chain runs exactly once, in MRO order. Three rules make cooperative inheritance work:
- Every class calls
super().__init__(...). - Every class accepts
**kwargsand passes on what it does not consume. - There is a common base at the bottom that stops the chain.
class Broken(Base):
def __init__(self, value, **kwargs):
self.value = value
# no super() call - everything after this class in the MRO is skippedMixins
A mixin is a small class that adds one capability and is never instantiated on its own. It is the most defensible use of multiple inheritance.
import json
class ReprMixin:
"""Adds a readable repr based on the instance dictionary."""
def __repr__(self):
fields = ", ".join(f"{k}={v!r}" for k, v in vars(self).items())
return f"{type(self).__name__}({fields})"
class JsonMixin:
"""Adds JSON serialisation."""
def to_json(self, **kwargs):
return json.dumps(vars(self), default=str, **kwargs)
class ComparableMixin:
"""Compares instances by their attribute dictionaries."""
def __eq__(self, other):
return type(self) is type(other) and vars(self) == vars(other)
class Product(ReprMixin, JsonMixin, ComparableMixin):
def __init__(self, name, price):
self.name = name
self.price = price
p = Product("Notebook", 250)
print(p)
print(p.to_json())
print(p == Product("Notebook", 250)) # TrueMixin conventions
- Name it ending in
Mixin. - Put mixins before the main base class, so they can override it.
- Give each one a single capability.
- Do not give a mixin its own
__init__unless it cooperates properly withsuper(). - Never instantiate a mixin directly.
Inspecting a hierarchy
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
print(D.__bases__) # the direct parents
print([c.__name__ for c in D.__mro__]) # the full lookup order
print(D.__subclasses__()) # direct children
print(issubclass(D, A), isinstance(D(), A)) # True True
# Where does a name actually come from?
for cls in D.__mro__:
if "hello" in cls.__dict__:
print("hello is defined in", cls.__name__)
breakCommon mistakes
- Assuming
super()means "my parent". It means "the next class in the MRO". - Omitting a
super()call, silently skipping the rest of the chain. - Calling
Parent.__init__(self, ...)directly, which breaks cooperative inheritance. - Not accepting
**kwargsin a cooperative__init__, so arguments cannot pass through. - Listing base classes in an order that has no consistent MRO.
- Using multiple inheritance where composition would be clearer.
Best practices
- Prefer composition; use multiple inheritance mainly for mixins.
- Print
__mro__whenever the resolution order is not obvious. - Make every class in a cooperative chain call
super()and accept**kwargs. - Keep mixins small, single purpose and stateless.
- Put mixins first in the base class list.
Practice
- Build a diamond hierarchy and print the MRO, predicting it first.
- Write four classes where each
__init__cooperates, and prove each runs once. - Create an inconsistent MRO deliberately and read the error message.
- Write three mixins and combine them into one class.
- Given a class with five ancestors, write code that reports which one defines a given method.
Conclusion
Multiple inheritance is unambiguous in Python because the MRO is a single ordered list computed once. super() follows that list rather than the class tree, cooperative __init__ chains depend on every class calling it, and mixins are where all of this genuinely pays off.