Inheritance, MRO, and super()
Single vs multiple inheritance, C3 linearisation, cooperative super, and why composition usually wins.
Why this matters
Used well, inheritance reduces duplication and clarifies hierarchies. Used poorly, it produces unmaintainable diamond-shaped messes. This lesson teaches when to inherit, when to compose, how Python resolves multiple inheritance, and how Abstract Base Classes formalise "interfaces."
Learning objectives
- Use single inheritance correctly with
super(). - Read and explain Method Resolution Order (MRO).
- Use multiple inheritance and mixins responsibly.
- Define abstract base classes with
abc. - Know when to compose rather than inherit.
1. Single inheritance
class Animal:
def __init__(self, name: str) -> None:
self.name = name
def speak(self) -> str:
return "..."
class Dog(Animal):
def speak(self) -> str:
return "Woof"
class Cat(Animal):
def __init__(self, name: str, indoor: bool = True) -> None:
super().__init__(name) # delegate to parent
self.indoor = indoor
def speak(self) -> str:
return "Meow"super() finds the parent in the MRO and calls into it. Always use it; never call Animal.__init__(self, name) by name (breaks under multiple inheritance).
isinstance, issubclass
d = Dog("Rex")
isinstance(d, Dog) # True
isinstance(d, Animal) # True (subclasses count)
issubclass(Dog, Animal) # True
isinstance(d, (Dog, Cat)) # tuple of optionsIn type hints, prefer Protocol over isinstance for duck typing (Phase 2.4 + 3.5).
2. Method Resolution Order (MRO)
When you call obj.method(), Python walks the MRO until it finds a definition.
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
D.__mro__
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)CPython uses C3 linearisation to compute MRO. The rules guarantee:
- A subclass appears before its parents.
- Left-to-right order in the bases list is preserved.
- The MRO is consistent (no contradictions).
If you write an inconsistent hierarchy, Python raises TypeError: Cannot create a consistent method resolution order. Restructure.
3. super() and cooperative multiple inheritance
class Loggable:
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) # crucial
print(f"Created {self}")
class Saveable:
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._dirty = False
class Document(Loggable, Saveable):
def __init__(self, title: str) -> None:
super().__init__()
self.title = titleThe pattern:
- Every class in the chain accepts
*args, **kwargsand callssuper().__init__(*args, **kwargs). Document.__init__starts the chain:super().__init__()walks Loggable โ Saveable โ object.
This is cooperative multiple inheritance: each mixin participates without knowing the others.
Without super(), only the leftmost base would initialise. With super(), every base does.
4. Mixins โ small classes that add a feature
Mixins are classes designed to be mixed into others, not used alone.
class JsonSerialisableMixin:
def to_json(self) -> str:
import json
return json.dumps(self.__dict__)
class TimestampMixin:
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
from datetime import datetime
self.created_at = datetime.now()
class User(JsonSerialisableMixin, TimestampMixin):
def __init__(self, name: str) -> None:
super().__init__()
self.name = name
u = User("Ada")
u.to_json() # '{"created_at": "2026-...", "name": "Ada"}'Mixin conventions:
- Name ends with
Mixin. - Doesn't define
__init__(or chains viasuper()). - Each mixin adds one capability.
Pythonic alternative for many "mixin" cases: just a function, or composition.
5. Abstract Base Classes (ABCs)
Mark a class as abstract โ you can't instantiate it directly, and subclasses must implement specified methods.
from abc import ABC, abstractmethod
class Storage(ABC):
@abstractmethod
def save(self, key: str, value: bytes) -> None: ...
@abstractmethod
def load(self, key: str) -> bytes: ...
class S3Storage(Storage):
def save(self, key: str, value: bytes) -> None:
...
def load(self, key: str) -> bytes:
...
# Storage() # TypeError: can't instantiate abstract class
# class Bad(Storage): pass; Bad() # TypeError: missing abstract methodsUse ABCs for:
- Plugin interfaces.
- "All implementations must provide these methods."
- Pre-Python-3.8 way to express interfaces (now usually replaced by
Protocol, see Phase 2.4).
abc.abstractproperty, abc.abstractclassmethod exist but are deprecated; combine @property / @classmethod with @abstractmethod:
class Shape(ABC):
@property
@abstractmethod
def area(self) -> float: ...Virtual subclasses
A class can be registered as a subclass without inheriting:
class MyCollection: ...
from collections.abc import Sized
Sized.register(MyCollection)
isinstance(MyCollection(), Sized) # True (lies!)Useful for declaring conformance with collections.abc types when inheritance is undesirable. Rare; Protocol is usually cleaner.
6. collections.abc โ the ABCs you'll actually use
| ABC | If you implement... | You get... |
|---|---|---|
Iterable | __iter__ | for-loop support |
Iterator | __iter__, __next__ | |
Sized | __len__ | len() |
Container | __contains__ | in |
Hashable | __hash__ | usable as dict key |
Collection | the above three | |
Sequence | __getitem__, __len__ | index, count, reversal |
MutableSequence | + __setitem__, __delitem__, insert | append, extend, pop, ... |
Mapping | __getitem__, __len__, __iter__ | get, keys, values, ... |
MutableMapping | + __setitem__, __delitem__ | update, pop, ... |
Set | __contains__, __iter__, __len__ | |, &, -, ^ |
Callable | __call__ |
Subclass these to get many methods "for free." Or check isinstance(x, collections.abc.Mapping) to write generic code.
7. Composition over inheritance
The standard refactor when inheritance feels wrong:
# Inheritance โ Car IS-A Engine? No.
class Engine:
def start(self) -> None: ...
class Car(Engine):
pass
# Composition โ Car HAS-A Engine. Right.
class Car:
def __init__(self, engine: Engine) -> None:
self.engine = engine
def start(self) -> None:
self.engine.start()Reach for inheritance when:
- The relationship is truly "is-a."
- You want polymorphism (callers treat subclasses identically).
- You're extending a framework that requires it (e.g.,
Exception,unittest.TestCase).
Reach for composition when:
- You're sharing implementation, not type.
- You want to swap parts at runtime (DI).
- The hierarchy would be more than 2-3 levels deep.
8. Common inheritance traps
- Diamond problem: solved by C3 linearisation +
super(). But unclear hierarchies still produce bugs. Keep depth โค 3. - Calling parent by name (
Animal.__init__(self, ...)) skips MRO; breaks under multiple inheritance. - LSP violations: a subclass that requires more or returns less than its parent. (
Square(Rectangle)is the textbook example.) - Mutable class attributes:
class Foo: tags = []is shared across instances. Use__init__to set instance state. - Overriding without
super()when the parent has side effects.
9. Worked example: a small ORM-like pattern
from abc import ABC, abstractmethod
class Model(ABC):
table: str = "" # subclasses set this
def __init_subclass__(cls, **kwargs) -> None:
super().__init_subclass__(**kwargs)
if not cls.table:
raise TypeError(f"{cls.__name__} must define 'table'")
@classmethod
@abstractmethod
def from_row(cls, row: tuple) -> "Model": ...
@abstractmethod
def to_row(self) -> tuple: ...
class User(Model):
table = "users"
def __init__(self, uid: int, name: str) -> None:
self.uid = uid; self.name = name
@classmethod
def from_row(cls, row): return cls(*row)
def to_row(self): return (self.uid, self.name)
# Will raise at subclass creation:
# class Broken(Model): pass -> TypeError: Broken must define 'table'__init_subclass__ validates subclass declarations at class-definition time (not later when you instantiate). Cheap form of "linting."
Hands-on lab (2 hours)
- Build an
Animalhierarchy withDog,Cat,Cowpolymorphically callingspeak(). - Add a
LoudMixinthat wrapsspeak()to UPPERCASE; mix it in; verify MRO. - Define an
abstractNotifierABC withsend(message)and writeEmailNotifier,SmsNotifier. - Build a class hierarchy that triggers
TypeError: Cannot create a consistent method resolution order; fix it. - Implement a
Sequence-conforming class by inheriting fromcollections.abc.Sequence; observe what methods you get for free. - Refactor a 3-level inheritance chain into composition; compare readability.
- Bonus: write
__init_subclass__that registers all subclasses in a dict โ implement a tiny plugin loader.
Common pitfalls
- Calling parent's
__init__by name โ breaks multiple inheritance. - Mixin defining
__init__withoutsuper(). - Deep hierarchies; nobody can follow them.
- Mutable class attribute as instance default.
- Inheriting where composition is clearer.
Self-check
- What does
super()do? - How is MRO computed?
- When use ABC vs Protocol?
- State the Liskov Substitution Principle.
- State two cases where composition beats inheritance.
References
- Fluent Python, Ramalho โ Chapters 13, 14.
- Python Cookbook โ Chapter 8.
- Raymond Hettinger, "Super considered super!" (PyCon 2015).
- PEP 3119 โ Abstract Base Classes.
- PEP 487 โ Simpler subclass customisation.
Sign in to save your progress and earn badges.