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.
Before this lesson: Python Classes and Objects, Python Inheritance
2Python Composition: diagram
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
Manageris anEmployee" sounds right, so inheritance fits. - "A
Caris anEngine" sounds silly. "ACarhas anEngine" sounds right, so the car should hold an engine object. - "An
Orderhas order lines", "aLibraryhas books", "aPlaylisthas 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:
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:
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:
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:
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:
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.
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.
- Line 1-4
Songis a small, self-contained class with two attributes. A song knows nothing about playlists. - Line 6-7
label()lets a song describe itself astitle (m:ss). Whoever holds a song can simply ask it for its label instead of repeating the formatting. - Line 9-12
Playlistis where composition happens.self.songs = []means a playlist has a list ofSongobjects. The list is created inside__init__, so every playlist gets its own. - Line 14-15
add()accepts a finishedSongobject and stores it in the list. The playlist does not build songs, it just holds them. - Line 17-18
total_seconds()is an aggregate method: it walks through the child objects and adds up theirsecondsattributes. - Line 20-22Create the playlist, then create two
Songobjects and hand them over withadd(). - Line 23-24The loop reaches the songs through the playlist and delegates the formatting to each song's own
label(): 215 seconds is3:35and 187 seconds is3:07. - 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)✓ 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())✓ 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())✓ 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)✓ 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 ✓ CompletedBattery is given. Write a class Laptop that has a battery:
__init__(self, model, battery)stores both onself(asmodelandbattery).work(self, hours)delegates to the battery by callingself.battery.drain(...)withhours * 15.charge_left(self)returns the battery'slevel.
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.work, do not change the level yourself. Hand the job to the inner object: self.battery.drain(hours * 15).class Battery:
def __init__(self, level=100):
self.level = level
def drain(self, amount):
self.level = max(0, self.level - amount)
class Laptop:
def __init__(self, model, battery):
self.model = model
self.battery = battery
def work(self, hours):
self.battery.drain(hours * 15)
def charge_left(self):
return self.battery.level
laptop = Laptop("Aero", Battery())
laptop.work(2)
print(laptop.charge_left())
2. Predict the shared song
Predict the output Medium ✓ CompletedTwo 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.
add() stores a reference to the same Song object. When hit.title changes, every playlist holding that object sees the change.['Sunrise (remix)']
['Sunrise (remix)', 'Highway']
1 2
2 more exercises → 5 more examples
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