Inheritance, MRO, and Composition
Inheritance, MRO, and Composition
After reading this chapter, you will master single and multiple inheritance in Python, understand the Method Resolution Order (MRO) derived by C3 linearization, implement cooperative multiple inheritance using super(), enforce architectural interfaces using Abstract Base Classes (abc.ABC), and favor composition over fragile inheritance hierarchies.
Mental model
When a subclass inherits from multiple parent classes, how does Python decide which implementation of a method to execute? It computes a deterministic, monotonic linear order known as the Method Resolution Order (MRO) using the C3 Linearization Algorithm.
Diamond Inheritance Graph:
[ Base ]
/ \
[ Left ] [ Right ]
\ /
[ Child ]
C3 Linearization (Child.__mro__):
Child ──▶ Left ──▶ Right ──▶ Base ──▶ object
Cooperative super() Dispatch
In Python, super() does not simply call the immediate parent in the source code. Instead, it calls the next class in the instance’s MRO:
Call from Child: super().process()
1. Inspects type(self).__mro__
2. Finds caller's current class in MRO (Child)
3. Dispatches to the NEXT class in sequence (Left)
4. Left's super().process() dispatches to Right!
This cooperative chaining ensures every class in the graph is visited exactly once.
Minimal example
Save as inheritance_mro.py:
# inheritance_mro.py
class BaseService:
def __init__(self, name: str) -> None:
self.name = name
print(f" [BaseService] Initialized service: {name}")
class LoggingMixin:
def __init__(self, *args: object, **kwargs: object) -> None:
super().__init__(*args, **kwargs)
print(" [LoggingMixin] Telemetry logger attached.")
class MetricsMixin:
def __init__(self, *args: object, **kwargs: object) -> None:
super().__init__(*args, **kwargs)
print(" [MetricsMixin] Metrics reporter registered.")
# Multiple inheritance with Mixins
class ProductionWebService(LoggingMixin, MetricsMixin, BaseService):
def __init__(self, name: str) -> None:
print(f"Initializing ProductionWebService: {name}")
# Cooperative super() walks through LoggingMixin -> MetricsMixin -> BaseService
super().__init__(name)
def main() -> None:
svc = ProductionWebService("auth-gateway")
print("\nMethod Resolution Order (MRO):")
for idx, cls in enumerate(ProductionWebService.__mro__):
print(f" {idx}: {cls.__name__}")
if __name__ == "__main__":
main()Run via uv run python inheritance_mro.py:
Initializing ProductionWebService: auth-gateway
[BaseService] Initialized service: auth-gateway
[MetricsMixin] Metrics reporter registered.
[LoggingMixin] Telemetry logger attached.
Method Resolution Order (MRO):
0: ProductionWebService
1: LoggingMixin
2: MetricsMixin
3: BaseService
4: object
Worked examples
Case 1: Formal Interfaces with Abstract Base Classes (abc.ABC)
In plugin systems, drivers, and microservices, you must guarantee that subclasses adhere to a specific interface. Abstract Base Classes prevent instantiation if required methods are missing:
# storage_plugin_abc.py
from abc import ABC, abstractmethod
class StorageDriver(ABC):
"""Abstract base contract for blob and file storage backends."""
@abstractmethod
def read_bytes(self, path: str) -> bytes:
"""Fetch raw bytes from storage."""
...
@abstractmethod
def write_bytes(self, path: str, data: bytes) -> int:
"""Write raw bytes and return bytes written."""
...
def ping(self) -> bool:
"""Concrete utility method available to all implementations."""
return True
class MemoryStorageDriver(StorageDriver):
def __init__(self) -> None:
self._store: dict[str, bytes] = {}
def read_bytes(self, path: str) -> bytes:
if path not in self._store:
raise FileNotFoundError(f"Path not found: {path}")
return self._store[path]
def write_bytes(self, path: str, data: bytes) -> int:
self._store[path] = data
return len(data)
if __name__ == "__main__":
driver = MemoryStorageDriver()
print("Driver ping:", driver.ping())
n = driver.write_bytes("/configs/app.json", b'{"debug": true}')
print(f"Wrote {n} bytes.")
print("Read back:", driver.read_bytes("/configs/app.json"))
# Attempting to instantiate an incomplete subclass fails:
class IncompleteDriver(StorageDriver):
pass
try:
IncompleteDriver()
except TypeError as err:
print("\nInstantiation rejected for incomplete subclass:")
print(f" {err}")Run:
uv run python storage_plugin_abc.pyOutput:
Driver ping: True
Wrote 15 bytes.
Read back: b'{"debug": true}'
Instantiation rejected for incomplete subclass:
Can't instantiate abstract class IncompleteDriver without an implementation for abstract methods 'read_bytes', 'write_bytes'
Case 2: Composition Over Inheritance
Deep inheritance hierarchies (Device -> NetworkDevice -> Switch -> ManagedSwitch -> CiscoManagedSwitch) become rigid, fragile, and hard to test. Composition assembles behavior from independent components:
# composition_architecture.py
class SSHTransport:
def execute(self, host: str, command: str) -> str:
return f"[SSH to {host}] Executed: {command} -> Output: SUCCESS"
class RESTTransport:
def execute(self, host: str, command: str) -> str:
return f"[REST API to {host}] POST /rpc -> Output: 200 OK"
class NetworkSwitchManager:
"""Uses composition: receives a transport rather than inheriting from one."""
def __init__(self, host: str, transport: SSHTransport | RESTTransport) -> None:
self.host = host
self.transport = transport
def reload_interfaces(self) -> str:
return self.transport.execute(self.host, "reload interfaces")
if __name__ == "__main__":
# Swap transports at runtime without changing the Manager class
ssh_manager = NetworkSwitchManager("switch-01.rack-a", SSHTransport())
api_manager = NetworkSwitchManager("switch-02.rack-b", RESTTransport())
print(ssh_manager.reload_interfaces())
print(api_manager.reload_interfaces())Run:
uv run python composition_architecture.pyOutput:
[SSH to switch-01.rack-a] Executed: reload interfaces -> Output: SUCCESS
[REST API to switch-02.rack-b] POST /rpc -> Output: 200 OK
Pitfalls
Pitfall 1: Calling Direct Parent Names Instead of super()
Directly invoking ParentClass.__init__(self) bypasses cooperative multiple inheritance and causes sibling mixins in the MRO to be silently skipped:
# THE BUG:
class BrokenService(LoggingMixin, BaseService):
def __init__(self, name):
BaseService.__init__(self, name) # BUG: Skips LoggingMixin.__init__ completely!
# THE FIX:
class CorrectService(LoggingMixin, BaseService):
def __init__(self, name):
super().__init__(name) # Traverses all classes in __mro__ cooperativelyPitfall 2: Overusing Multiple Inheritance
Multiple inheritance is best reserved for lightweight, orthogonal mixins that do not maintain complex conflicting state. For domain business logic, prefer composition.
Exercises
- Define an Abstract Base Class
MessageQueue(ABC)with abstract methodspublish(topic: str, payload: bytes)andsubscribe(topic: str). Implement a concreteInMemoryQueuesubclass. - Given classes
A,B(A),C(A), andD(B, C), printD.__mro__and trace the order in which C3 linearization resolves methods. - Refactor a deep three-level inheritance tree into a single class that uses composition with two injected helper objects.
- Implement a
JSONSerializableMixinthat provides ato_json()method by serializingself.__dict__.
Further reading
- Guido van Rossum: The Python 2.3 Method Resolution Order.
- PEP 3119: Introducing Abstract Base Classes.
- Python Standard Library:
abcmodule.