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 out

Notice 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

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.

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.

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.

Nothing but those two methods on the object. Duck typing calls the method rather than checking the type, so no shared ancestor is required.

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

Back to Inheritance, composition and polymorphism: work through the checklist