Python Composition
Object-Oriented Python Object-Oriented Python

Python Composition: Has-A vs Is-A

1Python Composition: overview

Composition means building a class out of other objects it holds as attributes (a Car has an Engine) and delegating work to them, instead of inheriting from them; use inheritance for "is-a" relationships and composition for "has-a" ones.

GoalBy the end you can decide between "is-a" and "has-a", store other objects as attributes, delegate work to them, swap parts without changing the outer class, keep a list of child objects with aggregate methods, and refactor a wrong inheritance into composition.

Before this lesson: Python Classes and Objects, Python Inheritance

2Python Composition: diagram

Python Composition: Has-A vs Is-AInheritance: Car is-a Engine (wrong)class Car(Engine): ...car.rev() # a car is not an enginecar.horsepower # leaks into Carthe engine can never be swappedComposition: Car has-a Engine (right)class Car: def __init__(self, engine): self.engine = engine def start(self): return self.engine.start()refactor
On the left the Car pretends to be an Engine, so every engine method leaks into Car and the engine can never change. On the right the Car holds an engine object and forwards the work to it, so you can swap the engine without touching Car.

3Python Composition: explained step by step

When you want one class to reuse what another class already does, Python gives you two tools. Inheritance says a class is a kind of another class. Composition says a class has another object inside it and asks that object to do part of the work. Picking the right one early saves a lot of pain later.

The "is-a" and "has-a" test

Say the relationship out loud as a sentence:

  • "A Manager is an Employee" sounds right, so inheritance fits.
  • "A Car is an Engine" sounds silly. "A Car has an Engine" sounds right, so the car should hold an engine object.
  • "An Order has order lines", "a Library has books", "a Playlist has songs": all composition.

A second test: if you inherit, the child gets every public method of the parent. Ask whether every one of them makes sense on the child. A car that suddenly has engine.rev() and engine.horsepower built in is a warning sign.

An object that holds other objects

Composition needs nothing new. Attributes can hold any value, including other objects. Pass the inner object into __init__, store it on self, and let the outer method delegate, which means it asks the inner object to do the real work:

Python
class Engine:
    def __init__(self, horsepower):
        self.horsepower = horsepower
        self.running = False

    def start(self):
        self.running = True
        return f"engine with {self.horsepower} hp is running"

class Car:
    def __init__(self, model, engine):
        self.model = model
        self.engine = engine

    def start(self):
        return f"{self.model}: " + self.engine.start()

car = Car("Roadster", Engine(150))
print(car.start())
print(car.engine.running)

Car does not know how an engine starts. It only knows that its engine has a start() method. All engine details stay inside Engine, so a change there does not force a change in Car.

Small parts you can swap

Because Car just calls self.engine.start(), it works with any object that has a start() method. This is duck typing from the polymorphism lesson put to work: if it quacks like an engine, Car is happy. You can build a different car by passing a different part, or even swap the part later:

Python
class PetrolEngine:
    def start(self):
        return "vroom"

class ElectricEngine:
    def start(self):
        return "hummmm"

class Car:
    def __init__(self, model, engine):
        self.model = model
        self.engine = engine

    def start(self):
        return f"{self.model} says {self.engine.start()}"

for engine in (PetrolEngine(), ElectricEngine()):
    print(Car("Roadster", engine).start())

pickup = Car("Pickup", PetrolEngine())
pickup.engine = ElectricEngine()
print(pickup.start())

With inheritance this would be much harder: Car would be tied to one parent for its whole life, and you would need a new subclass for every combination.

Refactoring a wrong inheritance

A very common trap is inheriting just to reuse code. Say you need a stack, where you may only add and remove at the top. Python's list already stores items, so class Stack(list) looks like a shortcut:

Python
class Stack(list):
    def push(self, item):
        self.append(item)

pages = Stack()
pages.push("home")
pages.push("search")
pages.insert(0, "oops")
print(pages.pop())
print(pages.pop(0))

A Stack is not a list. It inherited dozens of methods such as insert, sort and pop(0) that let anyone break the "last in, first out" rule. The fix is to have a list instead of being one, and to offer only the methods a stack should have:

Python
class Stack:
    def __init__(self):
        self._items = []

    def push(self, item):
        self._items.append(item)

    def pop(self):
        return self._items.pop()

    def peek(self):
        return self._items[-1]

    def __len__(self):
        return len(self._items)

pages = Stack()
pages.push("home")
pages.push("search")
print(pages.pop(), len(pages))
print(hasattr(pages, "insert"))

The list is now an implementation detail. You could replace it with a different structure tomorrow and nobody using Stack would notice.

Holding many objects

Often the inner part is a whole collection. An Order has many OrderLine objects. The outer class keeps them in a list it creates itself, and aggregate methods answer questions about the whole by asking each part:

Python
class OrderLine:
    def __init__(self, product, unit_price, quantity):
        self.product = product
        self.unit_price = unit_price
        self.quantity = quantity

    def subtotal(self):
        return self.unit_price * self.quantity

class Order:
    def __init__(self, customer):
        self.customer = customer
        self.lines = []

    def add(self, product, unit_price, quantity=1):
        self.lines.append(OrderLine(product, unit_price, quantity))

    def total(self):
        return sum(line.subtotal() for line in self.lines)

order = Order("Asha")
order.add("notebook", 3.50, 2)
order.add("pen", 1.25, 4)
print(len(order.lines), order.total())

Order.total() does not repeat the price-times-quantity maths. It delegates that to each OrderLine. Each class stays small and has one job.

Composition over inheritance

Experienced programmers often say "favour composition over inheritance". The reasons are practical:

  • A child is tied to its parent's internals; a change in the parent can break every child.
  • Inheritance is decided when you write the class, composition can change while the program runs.
  • Parts are easy to test on their own, and easy to replace with a simple stand-in.
  • You expose only the methods you choose, instead of everything the parent has.

When inheritance is still the right tool

It is not a bad idea, just a narrower one. Use it when the child really is a specialised version of the parent and can be used anywhere the parent is expected: a Manager in code that handles Employee objects, or your own error class that extends Exception. Many designs use both: Employee(Person) is a person and has an address.

A good habit: start with composition. Reach for inheritance when you can honestly say "is a" and you want the polymorphism that comes with it.

4Python Composition: syntax

class Part:
    def work(self):
        return "done"

class Whole:
    def __init__(self, part):
        self.part = part                 # Whole HAS-A Part: store the object on self

    def work(self):
        return self.part.work()          # delegate: the inner object does the real job

class Container:
    def __init__(self):
        self.items = []                  # HAS many child objects (a new list per object)

    def add(self, item):
        self.items.append(item)

    def total(self):
        return sum(item.price for item in self.items)    # aggregate over the children

thing = Whole(Part())                    # hand the part in: easy to replace
thing.part = OtherPart()                 # ...or swap it later (any object with work())

5Python Composition: run the example

Press Run (or Ctrl + Enter). The output (step 6) appears directly below the code. Change anything and run it again.

Interactive example
class Song:
    def __init__(self, title, seconds):
        self.title = title
        self.seconds = seconds

    def label(self):
        return f"{self.title} ({self.seconds // 60}:{self.seconds % 60:02d})"

class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []

    def add(self, song):
        self.songs.append(song)

    def total_seconds(self):
        return sum(song.seconds for song in self.songs)

mix = Playlist("Road trip")
mix.add(Song("Sunrise", 215))
mix.add(Song("Highway", 187))
for song in mix.songs:
    print(song.label())
print(mix.name, "lasts", mix.total_seconds(), "seconds")
Expected outputSunrise (3:35)
Highway (3:07)
Road trip lasts 402 seconds

7Python Composition: how the example works

Hover or tap a step to highlight its lines in the example above.

  1. Line 1-4Song is a small, self-contained class with two attributes. A song knows nothing about playlists.
  2. Line 6-7label() lets a song describe itself as title (m:ss). Whoever holds a song can simply ask it for its label instead of repeating the formatting.
  3. Line 9-12Playlist is where composition happens. self.songs = [] means a playlist has a list of Song objects. The list is created inside __init__, so every playlist gets its own.
  4. Line 14-15add() accepts a finished Song object and stores it in the list. The playlist does not build songs, it just holds them.
  5. Line 17-18total_seconds() is an aggregate method: it walks through the child objects and adds up their seconds attributes.
  6. Line 20-22Create the playlist, then create two Song objects and hand them over with add().
  7. Line 23-24The loop reaches the songs through the playlist and delegates the formatting to each song's own label(): 215 seconds is 3:35 and 187 seconds is 3:07.
  8. Line 25The playlist's total is 215 + 187 = 402 seconds.

8Python Composition: common mistakes

Inheriting just to reuse code

✗ Wrong

class Engine:
    def __init__(self, horsepower):
        self.horsepower = horsepower

    def start(self):
        return "engine running"

class Car(Engine):
    pass

car = Car(150)
print(isinstance(car, Engine))
print(car.horsepower)
Prints True and 150. There is no error, but Python now treats a Car as an Engine: Car(150) really means "an engine with 150 hp", and the engine can never be replaced.

✓ Right

class Engine:
    def __init__(self, horsepower):
        self.horsepower = horsepower

    def start(self):
        return "engine running"

class Car:
    def __init__(self, model, engine):
        self.model = model
        self.engine = engine

    def start(self):
        return self.engine.start()

car = Car("Roadster", Engine(150))
print(isinstance(car, Engine))
print(car.engine.horsepower, car.start())

A car is not a kind of engine. Store an engine object inside the car (self.engine = engine) and call its methods from the car's own methods.

Forgetting self when storing the part

✗ Wrong

class Engine:
    def start(self):
        return "engine running"

class Car:
    def __init__(self, engine):
        engine = engine

    def start(self):
        return self.engine.start()

car = Car(Engine())
print(car.start())
AttributeError: 'Car' object has no attribute 'engine'

✓ Right

class Engine:
    def start(self):
        return "engine running"

class Car:
    def __init__(self, engine):
        self.engine = engine

    def start(self):
        return self.engine.start()

car = Car(Engine())
print(car.start())

engine = engine only creates a local variable that disappears when __init__ ends. Store the part on the object with self.engine = engine so other methods can use it.

Passing the class instead of an object

✗ Wrong

class Engine:
    def start(self):
        return "engine running"

class Car:
    def __init__(self, engine):
        self.engine = engine

    def start(self):
        return self.engine.start()

car = Car(Engine)
print(car.start())
TypeError: Engine.start() missing 1 required positional argument: 'self'

✓ Right

class Engine:
    def start(self):
        return "engine running"

class Car:
    def __init__(self, engine):
        self.engine = engine

    def start(self):
        return self.engine.start()

car = Car(Engine())
print(car.start())

Engine is the class itself, so Engine.start() has no object to use as self. Pass a real object, which means calling the class with parentheses: Engine().

One shared engine for every car

✗ Wrong

class Engine:
    def __init__(self):
        self.running = False

    def start(self):
        self.running = True

class Car:
    engine = Engine()

    def start(self):
        self.engine.start()

first = Car()
second = Car()
first.start()
print(second.engine.running)
Prints True. There is no error, but starting the first car also "started" the second one.

✓ Right

class Engine:
    def __init__(self):
        self.running = False

    def start(self):
        self.running = True

class Car:
    def __init__(self):
        self.engine = Engine()

    def start(self):
        self.engine.start()

first = Car()
second = Car()
first.start()
print(second.engine.running)

An object created in the class body is a class attribute, shared by every car. Create the part inside __init__ (or receive it as an argument) so each car gets its own.

9Python Composition: exercises

Write your answer, then press Check. The checks run in your browser.

1. Give a Laptop a Battery

Write code Easy

Battery is given. Write a class Laptop that has a battery:

  • __init__(self, model, battery) stores both on self (as model and battery).
  • work(self, hours) delegates to the battery by calling self.battery.drain(...) with hours * 15.
  • charge_left(self) returns the battery's level.
Your code
class Battery:
    def __init__(self, level=100):
        self.level = level

    def drain(self, amount):
        self.level = max(0, self.level - amount)

# Write class Laptop below.

2. Predict the shared song

Predict the output Medium

Two playlists hold the same Song object. Think about what a list stores (the object itself, not a copy) and type exactly what the program prints.

Read this code
class Song:
    def __init__(self, title):
        self.title = title

class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []

    def add(self, song):
        self.songs.append(song)

    def titles(self):
        return [song.title for song in self.songs]

hit = Song("Sunrise")
gym = Playlist("Gym")
trip = Playlist("Trip")
gym.add(hit)
trip.add(hit)
trip.add(Song("Highway"))
hit.title = "Sunrise (remix)"
print(gym.titles())
print(trip.titles())
print(len(gym.songs), len(trip.songs))

2 more exercises → 5 more examples

Finished this lesson?
Progress is saved in this browser — create a free account to keep it across devices.

Questions learners ask

What does "composition over inheritance" mean?

It is a design guideline: when you want to reuse behaviour, prefer giving a class other objects to work with (has-a) over making it a subclass (is-a). Composition keeps classes loosely connected, lets you swap parts, and avoids inheriting methods you do not want.

How do I decide between is-a and has-a?

Say the sentence out loud. If "X is a kind of Y" is true and X can be used anywhere Y is expected, use inheritance. If X uses or contains Y (a Car has an Engine, an Order has lines), store Y as an attribute. When you are unsure, choose composition.

What is the difference between composition and aggregation?

In strict terms, composition means the whole creates and owns its parts (an Order creates its OrderLines), while aggregation means the parts exist independently and are passed in (songs shared by several playlists). In everyday Python both are just attributes that hold objects, so the code looks the same.

Further reading

10Next lesson

Up nextPython Abstract Classes (ABC)An abstract class is a base class that lists the methods every child must provide without saying how they work, and Pyt…

Related:
Python InheritancePython Polymorphism and Duck TypingPython Multiple Inheritance and MRO

← Python Multiple Inheritance and MRO · All Object-Oriented Python lessons · Practise in the IDE