Embercode
✕
Object-Oriented ProgrammingLesson 7 of 17

40 min read

Composition over inheritance in Python

Why this matters

Inheritance is the first tool most beginners reach for when two classes look alike. A Car and a Truck both have wheels, so Truck inherits from Car. This works for a while. Then a new requirement arrives, such as a hybrid car that must start on electric power in one place and behave like a petrol car in another, and the class tree stops fitting the problem.

Composition is the other tool. You build a class out of smaller objects that it keeps in attributes. The big class asks those small objects to do the work. Interviewers often ask you to explain why you picked one over the other.

This lesson builds a car from an engine and wheels. It then shows the cases where a subclass is still the right choice. Encapsulation, which keeps attributes internal, is covered in an earlier lesson. Protocols and abstract base classes, which describe what an object can do without saying how, are covered in the lessons on polymorphism and on interfaces. Here the focus is on how the pieces are put together.

Two questions: "is a" and "has a"

Two plain-English questions decide most of these choices.

  • Is a. A dog is an animal. A square is a shape. If you can say "X is a kind of Y" and it stays true, inheritance may fit. In Python this is written with a class name in brackets, such as class Dog(Animal):.
  • Has a. A car has an engine. A car has four wheels. If the sentence is "X has a Y", composition fits. The car keeps an engine in an attribute.

Try the test on a car. "A car is an engine" sounds wrong, and it is. "A car has an engine" is true. So the car should hold an engine, not inherit from one. This one check removes many bad designs before they are written.

Inheritance is also a permanent link. A subclass takes on every public method its parent has. Once other code uses those, you cannot easily remove them. Composition is looser. The car holds an engine in an attribute, and the attribute can be replaced at any time.

The problem with inheritance first

Suppose you try to model two kinds of car with subclasses. You write a Vehicle class, then a PetrolCar that inherits from it and an ElectricCar that inherits from it too. So far so good. Now you need a HybridCar that has both a petrol engine and an electric motor, and the rule for hybrids is that they start on electric power.

A hybrid needs behaviour from two parents. Python allows a class to list two parents, so the first attempt runs. Here is the program:

Python · editable

The output is:

petrol car starts
['HybridCar', 'PetrolCar', 'ElectricCar', 'Vehicle', 'object']

The program runs, but the hybrid starts as a petrol car. Python searches the parents in the order shown in the second line, and it stops at the first start it finds, which is PetrolCar. The electric version is never used, and Python gives no warning. Multiple inheritance does not express "both", it only chooses one.

Composition solves this without needing two parents. The hybrid does not need to inherit from two classes. It needs a petrol engine and an electric motor, and it can hold both.

Building a car from parts

Start with the smallest piece: the engine. Real cars can have petrol engines, electric motors, or other kinds. In Python, the common way to say "anything that can start" is an abstract base class. It lists method names with no bodies. Any class that inherits from it and writes those methods can be used where the base is expected.

from abc import ABC, abstractmethod
 
 
class Engine(ABC):
    @abstractmethod
    def start(self):
        ...
 
    @property
    @abstractmethod
    def horsepower(self):
        ...

The base says what an engine can do. It does not say how. Two classes will provide it. The class name in brackets, Engine, is how a class says that it keeps the promise made by the base.

# Engine is the abstract base class from the block above.
class PetrolEngine(Engine):
    def __init__(self, horsepower):
        self._horsepower = horsepower
 
    def start(self):
        return "petrol engine: fuel and spark"
 
    @property
    def horsepower(self):
        return self._horsepower
 
 
class ElectricEngine(Engine):
    def __init__(self, horsepower):
        self._horsepower = horsepower
 
    def start(self):
        return "electric engine: battery to motor"
 
    @property
    def horsepower(self):
        return self._horsepower

The attribute _horsepower is internal, as the encapsulation lesson showed. The property horsepower gives read-only access to it, so the engine's power cannot be changed from outside. If you misspell a method name in a subclass, Python does not notice when the class is written. A type checker such as mypy reports it, and the abstract base class refuses to create a child that is missing a promised method.

Next is the wheel. A wheel is a small object that knows where it sits on the car.

class Wheel:
    def __init__(self, position):
        self._position = position
 
    @property
    def position(self):
        return self._position

Now the car. The car does not inherit from an engine or a wheel. It has them. The engine and the wheels are attributes, and the initializer receives them from the outside.

class Car:
    def __init__(self, model, engine, wheels):
        self._model = model
        self._engine = engine
        self._wheels = tuple(wheels)
 
    def drive(self):
        return f"{self._model} with {len(self._wheels)} wheels: {self._engine.start()}"
 
    def power(self):
        return self._engine.horsepower

Two details matter here. The _engine attribute is not typed as a PetrolEngine. The car calls start and horsepower on whatever it holds, so it does not know or care which kind of engine it has. The second detail is tuple(wheels). It makes a read-only copy of the sequence the caller passed in. If the caller later adds a fifth wheel to their own list, the car's copy does not change. The encapsulation lesson explains why this protects the car.

Here is the complete program. It defines the classes above and uses them.

Python · editable

The output is:

City A with 4 wheels: petrol engine: fuel and spark, 110 hp
City E with 4 wheels: electric engine: battery to motor, 150 hp

The two cars share one list of wheels, which is safe because Wheel objects are never changed after they are built. The Car class is written once. It works with any engine that follows the Engine promise, including engines you have not written yet.

The list four is an ordinary list, so it could still be changed by its owner. The car has its own tuple copy, so the car is not affected by such a change.

Changing a part while the program runs

A composed object can swap a part. This is hard to do with inheritance, because the class of an object is fixed when the object is created. An attribute can be changed at any time. The example below starts a car with a petrol engine, then replaces the engine with an electric one.

Python · editable

The output is:

petrol engine: fuel and spark
electric engine: battery to motor
PetrolEngine -> petrol engine: fuel and spark
ElectricEngine -> electric engine: battery to motor

The Car class did not change when the engine changed. drive asked whatever engine it held to start, and it got the answer from the engine it held at that moment. The list [PetrolEngine(), ElectricEngine()] shows that one kind of call works for either engine. The expression type(e).__name__ prints the name of the class of the object, which is useful for checking what you built.

The attribute _engine changes here, so it is written with a method that swaps it. An attribute that should never change needs no such method, and Python has no keyword that makes it fixed, so the rule is a convention.

A third example: a garage that forwards work

Composition also covers a pattern called delegation. Delegation means one object holds another and passes a request on to it. The first object does not do the work itself. The garage below keeps many cars in a dictionary keyed by number plate. A dictionary is a collection that finds a value from a key. The garage forwards a start request to the right car, and it adds up the horsepower of all its cars.

This example also uses shapes. A shape is a good case for inheritance, because a circle and a rectangle are both shapes in the plain sense, and both must answer area(). The example shows both tools working together.

Python · editable

The output is:

Circle with area 3.14
Rectangle with area 12.00
KA-02 -> electric: battery to motor
no car with plate KA-09
total hp: 260

The Garage never looks inside a car. It asks the car for its plate, its start message and its horsepower. If you later add a hybrid engine class, the garage needs no change. This is the practical benefit of composition: each class depends on a small promise, not on the inside of another class.

Notice how the shapes are written. Circle and Rectangle inherit from Shape because a circle is a shape in the everyday sense, and every shape must have an area. The method describe is written once in Shape and is inherited by both. Inheritance saves code here because the shared behaviour is real and the relationship is a true "is a".

The dictionary keeps the cars in the order they were parked, because Python dictionaries keep their insertion order. You do not need a special LinkedHashMap for that.

When inheritance is still the right tool

Composition is the default, but inheritance is not wrong in general. It fits when all of these are true:

  • The new class is a more specific kind of the parent, and everything true of the parent is true of the child.
  • The child can be used anywhere the parent is used, without the caller needing to know the difference. This is sometimes called the substitution rule.
  • The shared code is real behaviour, not just a few attributes that happen to match.

The Python standard library uses inheritance in clear cases. A ValueError is an Exception, and code that catches Exception should catch it too. A dict subclass such as OrderedDict or Counter can be used wherever a dictionary is expected. The abstract base class Shape above is a plain case of the same rule.

Now a case where inheritance looks right but is not. A stack is a list where you add to the top and remove from the top. A beginner might write this:

Python · editable

The output is:

inherited stack pops: second
contents: ['sneaked in at the bottom', 'first']

This runs, and push and pop_top work. The problem is what else comes with list. Every method of the list is now available on the stack, including insert, which puts an item at the bottom. A stack that allows that is no longer a stack. The name pop is also taken over from list, which is why the example uses pop_top. The next version holds a list in an internal attribute instead:

Python · editable

The output is:

composed stack pops: second
size left: 1

The composed stack has no way to add to the bottom, because the list is internal and only push, pop and size are offered. The rule that makes a stack a stack lives in one place, and outside code cannot break it. The lesson on invariants covers rules like this in more detail.

Common mistakes

Inheriting only to reuse a few lines. A beginner sees that a Report needs a print_line method that already exists in Printer, and writes class Report(Printer):. Now every public method of Printer is also public on Report. If the rule is "a report is a kind of printer", that is fine. If not, the report now carries printing methods it should not offer. The fix is to keep a Printer in an attribute and call its method.

Deep chains of parent classes. When each new class inherits from the one before it, changing a method near the top changes behaviour in every class below it. A composed design keeps each class close to the things it uses, so a change touches fewer places.

Changing an attribute that should be fixed. Python does not stop code from writing car._engine = something_else from outside the class. Keep the attribute internal, and offer a method such as swap_engine when a change is allowed. A read-only property shows the rule clearly when the value must never change.

Exposing the held object. If Car had a property that returned _engine, callers could change the engine's state through it. Composition only protects you if the held object stays internal, or if it is read-only. The engine in the car example has only read methods, so returning it is safe there. A mutable list of wheels returned as is would not be safe.

Writing a base class with one implementation and no reason. Composition needs something to hold. If a Car will only ever have one kind of engine, the base class adds a name without adding a choice. Add the base class when a second implementation exists or is clearly coming.

Practice

Exercise 1. A Coffee class should hold a Milk object and a Sugar object, and make a description such as coffee with milk and sugar. Write the three classes. Should Coffee inherit from Milk? Explain why in one sentence.

Show the answer

Coffee should not inherit from Milk, because a coffee is not a kind of milk. A coffee has milk, so it keeps a Milk in an attribute. A possible version is:

Python · editable

This prints coffee with milk and sugar.

Exercise 2. Use the Car and Engine code from this lesson. Add a third engine class, HybridEngine, whose start returns hybrid engine: battery first, and whose horsepower is 130. Change nothing in Car. Then create a car with the hybrid engine and print its drive message.

Show the answer

Add the class and use it in the program:

# Engine is the abstract base class from the lesson above.
class HybridEngine(Engine):
    def start(self):
        return "hybrid engine: battery first"
 
    @property
    def horsepower(self):
        return 130

Car needs no change, because it holds an engine, and HybridEngine provides the same promise as the others. A line such as Car("City H", HybridEngine(), four).drive() prints the hybrid message, and drive is the same method used for the petrol car.

Exercise 3. Explain in plain words why the following is a bad use of inheritance, and rewrite it with composition: class Logbook(Notebook):, where Notebook has write and erase methods and a Logbook should only allow write.

Show the answer

A logbook is not a kind of notebook in the sense that a notebook can erase. If Logbook inherits from Notebook, the erase method is inherited and can be called on a logbook, which breaks the rule. The rewrite keeps a notebook in an attribute and offers only record:

Python · editable

This prints wrote: engine checked. Callers of Logbook see only record. The erase behaviour is not offered, so it cannot be called by mistake.

Exercise 4. Name one case where inheritance is the better choice than composition, and say which of the three tests in this lesson it passes.

Show the answer

A Square inheriting from Shape is a good case. A square is a kind of shape, every shape has an area, and code that takes a Shape can call area on a square without knowing it is special. This passes the first test (a more specific kind of the parent) and the second (it can be used wherever the parent is used). The third test, real shared behaviour, is met if the square also uses describe from Shape.

Exercise 5. A Library class keeps a list of Book objects. A caller asks for library.books() and then adds a book to the list it gets back. Why is this a problem, and what one change to books() fixes it?

Show the answer

If books() returns the internal list, the caller's append changes the library's own data without going through any checks the library might want to make. The fix is to return a copy, for example return tuple(self._books). The caller then gets a read-only snapshot, and changes to it do not reach the library.

Summary

  • Ask "is a" or "has a" before you choose inheritance. Use inheritance only for a true "is a kind of" relationship.
  • Python lets a class list several parents, but the first match wins. A class that needs two sets of behaviour should keep objects that provide them.
  • An abstract base class names what a part can do. Holding that promise lets you swap parts without changing the class that holds them.
  • Delegation means passing a request to the object you hold, so each class depends on a small promise and not on another class's inside.
  • An inherited class exposes every public method of its parent. A stack that inherits from a list allows operations a stack should not allow.
  • Keep held objects in internal attributes, and return copies of mutable collections.
Check yourself: Composition (6 questions) →Practice this topic: coding problems for Object-Oriented Programming →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Module assignment: Library desk in Python