2.8 Inheritance, composition and polymorphism
Checked against the Python 3 documentation, August 2026
What this is and why it exists
Inheritance and composition are the two ways to build on code somebody else wrote, and you need both to extend the libraries ahead — a PyTorch dataset, a scikit-learn transformer — without breaking the contracts they rely on. Python's version is looser than most languages', because it also accepts anything that behaves correctly regardless of what it inherits from. This topic is about choosing well between the two, and the honest default is not the one most tutorials teach.
The vocabulary
- Inheritance — a class taking another's behaviour as its starting point; the child is a kind of the parent.
- Composition — a class holding another as an attribute and delegating to it; the object has a collaborator.
- Override — replacing an inherited method.
- MRO (method resolution order) — the fixed order in which Python searches classes for an attribute.
- super() — a call to the next class in that order, rather than to a named parent.
- Duck typing — the glossary's phrase: a style "which does not look at an object's type to determine if it has the right interface".
- Abstract base class — a class that declares methods a subclass must provide.
- Protocol — a description of the shape an object must have, checked by a type checker rather than by inheritance.
The mental model
Ask whether the relationship is is a or has a, and answer honestly. A TimestampedDataset that is a dataset with one extra field is inheritance. A Pipeline that owns a model, a scaler and a logger is composition — it is not a kind of any of them. Getting this wrong in the inheritance direction is the more expensive mistake, because a subclass is coupled to everything about its parent, including the parts that change.
When you do inherit, super() is how you cooperate with the parent rather than replace it. Overriding an initialiser and forgetting to call the parent's is one of the most common ways to break a library class: the parent's initialiser is where the machinery gets set up, and skipping it leaves an object that looks right and fails later, somewhere unrelated.
Multiple inheritance exists and Python resolves it with the method resolution order — a fixed, computed sequence of classes that is searched in turn. It is worth knowing what it is, because you will read library code that uses mixins, and worth being sparing with in your own: a name found three classes away in an order nobody has in their head is a debugging session you did not need. Visit multiple inheritance; do not live there.
Which brings the honest default: composition is usually the sturdier choice. A held object can be swapped, tested on its own, and changed without every subclass rippling. Inheritance is right when a library asks for it — when its documented extension point is "subclass this and override that method" — and less often otherwise. Read the library's documentation and follow the shape it expects; write your own code with composition unless a real is a relationship exists.
Duck typing is Python's third option and often the best one. The glossary describes a style in which the method or attribute is "called or used" rather than the type being checked first — "if it looks like a duck and quacks like a duck, it must be a duck". So a function that needs something with a fit and a predict method does not need those things to share an ancestor; anything with both works. An abstract base class is for when you want the requirement enforced at construction time, and a Protocol is for when you want it checked by your type checker without demanding inheritance at all — structural rather than nominal, which is the Python-shaped answer.
In code
Checked against the Python tutorial's Classes chapter and the typing reference.
from typing import Protocol
class Estimator(Protocol):
"""Anything with these two methods satisfies this, with no inheritance."""
def fit(self, x, y) -> None: ...
def predict(self, x): ...
class Scaler:
def __init__(self, factor=1.0):
self.factor = factor
def transform(self, x):
return [v * self.factor for v in x]
class Pipeline:
"""Composition: a pipeline HAS a scaler and a model; it IS neither."""
def __init__(self, scaler: Scaler, model: Estimator):
self.scaler = scaler
self.model = model
def fit(self, x, y):
self.model.fit(self.scaler.transform(x), y)
return self
def predict(self, x):
return self.model.predict(self.scaler.transform(x))
class LoggedScaler(Scaler):
"""Inheritance, done the way a library expects: extend, do not replace."""
def __init__(self, factor=1.0, logger=None):
super().__init__(factor) # never skip this
self.logger = logger
def transform(self, x):
out = super().transform(x)
if self.logger:
self.logger.info("scaled %d values", len(out))
return outNotice that Pipeline satisfies the Estimator protocol itself, without inheriting from anything — it has both methods, so anything expecting an estimator accepts it. That is duck typing doing real work, and it is why scikit-learn-style code composes so freely.
What you should now be able to explain or do
Decide between inheritance and composition from the is a / has a question, and defend the answer. Extend a library class without breaking it, including the call people forget. Say what the method resolution order is and why deep multiple inheritance is a place to visit. Explain duck typing in the glossary's terms. Say when to reach for an abstract base class and when a Protocol is better.
Check yourself
A class holds a model, a scaler and a logger. Inheritance or composition?
Composition. It is not a kind of any of them; it has all three. Inheriting from a collaborator couples you to everything about it, including what changes.
You override an initialiser in a library subclass and the object misbehaves later, somewhere unrelated. What did you forget?
The super() call. The parent's initialiser sets up machinery the rest of the class depends on, and skipping it produces an object that looks right and fails elsewhere.
What is the method resolution order?
The fixed sequence of classes Python searches for an attribute. It is what makes multiple inheritance well-defined — and what makes a name found three classes away hard to locate by eye.
What does a function need in order to accept "anything with fit and predict"?
Nothing but those two methods on the object. Duck typing calls the method rather than checking the type, so no shared ancestor is required.
Abstract base class or Protocol?
An abstract base class when you want the requirement enforced at construction and inheritance is acceptable. A Protocol when you want a type checker to verify the shape without demanding anybody inherit from you.
Go deeper
We haven't checked most of these for screen reader use yet.
Back to Inheritance, composition and polymorphism: work through the checklist