Python - Multiple Inheritance and MRO
Python allows a class to inherit from multiple parent classes simultaneously. While this provides powerful composition capabilities, it introduces complexity around method resolution—when a child...
Key Insights
- Python’s Method Resolution Order (MRO) uses C3 linearization to determine which parent class method gets called in multiple inheritance scenarios, preventing the diamond problem that plagues other languages
- The
super()function enables cooperative multiple inheritance by following the MRO chain, but only works correctly when all classes in the hierarchy use it consistently - Multiple inheritance is most useful for mixins—small, focused classes that add specific functionality without maintaining state or creating deep hierarchies
Understanding Multiple Inheritance
Python allows a class to inherit from multiple parent classes simultaneously. While this provides powerful composition capabilities, it introduces complexity around method resolution—when a child class and multiple parents define the same method, which one gets called?
class A:
def process(self):
return "A"
class B:
def process(self):
return "B"
class C(A, B):
pass
obj = C()
print(obj.process()) # Output: "A"
The output is “A” because Python searches left-to-right through the parent classes listed in the class definition. But this simple rule breaks down with more complex hierarchies.
The Diamond Problem and MRO
The diamond problem occurs when a class inherits from two classes that share a common ancestor:
class Base:
def __init__(self):
print("Base.__init__")
self.value = "base"
class Left(Base):
def __init__(self):
print("Left.__init__")
Base.__init__(self)
self.value = "left"
class Right(Base):
def __init__(self):
print("Right.__init__")
Base.__init__(self)
self.value = "right"
class Diamond(Left, Right):
def __init__(self):
print("Diamond.__init__")
Left.__init__(self)
Right.__init__(self)
d = Diamond()
# Output:
# Diamond.__init__
# Left.__init__
# Base.__init__
# Right.__init__
# Base.__init__
Notice Base.__init__ executes twice—wasteful and potentially problematic if Base manages resources or has side effects.
Python’s MRO solves this using the C3 linearization algorithm. View any class’s MRO:
print(Diamond.__mro__)
# (<class '__main__.Diamond'>, <class '__main__.Left'>,
# <class '__main__.Right'>, <class '__main__.Base'>, <class 'object'>)
print(Diamond.mro()) # Same output as a list
The MRO guarantees that each class appears only once and that parent classes appear after their children.
Cooperative Multiple Inheritance with super()
The super() function follows the MRO chain, enabling cooperative inheritance where each class calls the next in line:
class Base:
def __init__(self):
print("Base.__init__")
self.value = "base"
class Left(Base):
def __init__(self):
print("Left.__init__")
super().__init__()
self.left_value = "left"
class Right(Base):
def __init__(self):
print("Right.__init__")
super().__init__()
self.right_value = "right"
class Diamond(Left, Right):
def __init__(self):
print("Diamond.__init__")
super().__init__()
d = Diamond()
# Output:
# Diamond.__init__
# Left.__init__
# Right.__init__
# Base.__init__
print(d.left_value) # "left"
print(d.right_value) # "right"
Each super().__init__() calls the next class in the MRO, ensuring Base.__init__ executes exactly once. This only works when all classes use super() consistently.
Handling Arguments in Multiple Inheritance
Cooperative inheritance with varying __init__ signatures requires careful argument handling:
class LoggerMixin:
def __init__(self, **kwargs):
self.verbose = kwargs.pop('verbose', False)
super().__init__(**kwargs)
def log(self, message):
if self.verbose:
print(f"[LOG] {message}")
class TimestampMixin:
def __init__(self, **kwargs):
from datetime import datetime
self.created_at = datetime.now()
super().__init__(**kwargs)
class DataStore:
def __init__(self, name, **kwargs):
super().__init__(**kwargs)
self.name = name
self.data = {}
def save(self, key, value):
self.data[key] = value
class EnhancedStore(LoggerMixin, TimestampMixin, DataStore):
def __init__(self, name, **kwargs):
super().__init__(name=name, **kwargs)
def save(self, key, value):
self.log(f"Saving {key}={value}")
super().save(key, value)
store = EnhancedStore("mystore", verbose=True)
store.save("user", "alice")
# Output: [LOG] Saving user=alice
print(store.created_at) # 2024-01-15 10:30:45.123456
Using **kwargs throughout allows each class to extract its parameters and pass the rest along the MRO chain.
Practical Mixin Pattern
Mixins are small classes that add specific functionality without being instantiated independently:
class JSONSerializableMixin:
def to_json(self):
import json
return json.dumps(self.__dict__, default=str)
@classmethod
def from_json(cls, json_str):
import json
data = json.loads(json_str)
return cls(**data)
class ValidatableMixin:
def validate(self):
required = getattr(self, '_required_fields', [])
for field in required:
if not hasattr(self, field) or getattr(self, field) is None:
raise ValueError(f"Missing required field: {field}")
return True
class User(JSONSerializableMixin, ValidatableMixin):
_required_fields = ['username', 'email']
def __init__(self, username, email, age=None):
self.username = username
self.email = email
self.age = age
user = User("alice", "alice@example.com", 30)
user.validate() # OK
json_data = user.to_json()
print(json_data) # {"username": "alice", "email": "alice@example.com", "age": 30}
restored = User.from_json(json_data)
print(restored.username) # "alice"
Mixins should be stateless or manage only their specific concerns, avoiding complex initialization logic.
Method Resolution in Practice
When multiple parents define the same method, understanding MRO determines behavior:
class Parser:
def parse(self, data):
return f"Parser: {data}"
class Validator:
def parse(self, data):
return f"Validator: {data}"
class Transformer:
def parse(self, data):
return f"Transformer: {data}"
class Pipeline(Parser, Validator, Transformer):
pass
p = Pipeline()
print(p.parse("test")) # "Parser: test"
print(Pipeline.__mro__)
# (<class '__main__.Pipeline'>, <class '__main__.Parser'>,
# <class '__main__.Validator'>, <class '__main__.Transformer'>, <class 'object'>)
To call methods from specific parents:
class Pipeline(Parser, Validator, Transformer):
def parse(self, data):
parser_result = Parser.parse(self, data)
validator_result = Validator.parse(self, data)
return f"Combined: {parser_result}, {validator_result}"
p = Pipeline()
print(p.parse("test"))
# "Combined: Parser: test, Validator: test"
Abstract Base Classes with Multiple Inheritance
Combine ABCs with mixins for enforced interfaces:
from abc import ABC, abstractmethod
class Persistable(ABC):
@abstractmethod
def save(self):
pass
@abstractmethod
def load(self, id):
pass
class CacheMixin:
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._cache = {}
def get_cached(self, key):
return self._cache.get(key)
def set_cached(self, key, value):
self._cache[key] = value
class DatabaseModel(Persistable, CacheMixin):
def __init__(self, table_name):
super().__init__()
self.table_name = table_name
def save(self):
# Save to database
self.set_cached(self.table_name, "saved_data")
return f"Saved to {self.table_name}"
def load(self, id):
cached = self.get_cached(id)
if cached:
return cached
# Load from database
return f"Loaded {id} from {self.table_name}"
model = DatabaseModel("users")
print(model.save()) # "Saved to users"
Common Pitfalls
Inconsistent super() usage: Mixing direct parent calls with super() breaks the chain:
class Bad(Left, Right):
def __init__(self):
super().__init__() # Follows MRO
Right.__init__(self) # Calls Right directly, may re-initialize Base
Order matters: Parent class order affects MRO:
class Version1(Left, Right):
pass
class Version2(Right, Left):
pass
print(Version1.__mro__) # Left before Right
print(Version2.__mro__) # Right before Left
Deep hierarchies: Multiple inheritance works best with shallow, focused mixins rather than deep class trees.
Multiple inheritance in Python provides powerful composition capabilities when used judiciously. The MRO and super() mechanism handle complexity that would require design patterns in other languages. Focus on mixins for cross-cutting concerns, use **kwargs for argument flexibility, and always verify your MRO matches expectations.