Composition in Python: Model Has-a Relationships with Objects
You have a Car class. You have an Engine class. They are clearly related, so you write class Car(Engine) — and something feels off immediately.

Key topics
You have a Car class. You have an Engine class. They are clearly related, so you write class Car(Engine) — and something feels off immediately.
The code runs, maybe. But Car now claims to be an Engine, which is false. A car is not a kind of engine. A car has an engine. That one sentence is the whole idea behind composition in Python, and once you see it, you stop reaching for inheritance every time two classes appear in the same file.
The Problem: Two Classes That Are Related but Not the Same Kind of Thing
If you have written a basic class before, you already know the pieces: a class is a blueprint, an object is an instance built from that blueprint, and inheritance models an is-a relationship. A SportsCar is a Car. A Manager is an Employee. That works because the sentence survives being said out loud.
The trouble starts when beginners apply the same tool to a different kind of relationship. A Car and an Engine are related, so inheritance looks tempting. But reverse the sentence and it collapses: a car is not a kind of engine. Inheritance would hand Car every attribute and method Engine owns, whether Car wants them or not. You did not want a car that is an engine. You wanted a car that contains one.
That second relationship has a name. When one object holds another object as an attribute, we call it composition, and the relationship is has-a. The class doing the holding is the composite class (the container). The class being held is the component class (the thing contained).
Here is the decision rule I want you to carry through the rest of this article: if you cannot say "X is a Y" and mean it, inheritance is probably the wrong tool. Say the sentence first. Let the grammar make the decision for you.
Knowledge check
Check your understanding
Answer this question before you continue.
A Tiny Runnable Example: A Car That Has an Engine
Let's build the smallest version that actually runs. Save this as car.py.
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
def start(self):
return f"Engine with {self.horsepower} hp starting up."
class Car:
def __init__(self, make, engine):
self.make = make
self.engine = engine # composition happens here
def start(self):
return f"{self.make}: {self.engine.start()}"
engine = Engine(150)
car = Car("Hatchback", engine)
print(car.start())
Run it from your terminal:
python car.py
Expected output:
Hatchback: Engine with 150 hp starting up.
Look at the line marked # composition happens here. That is the entire mechanism. self.engine = engine stores an Engine instance as an attribute of Car. No special syntax, no decorator, no inheritance. An attribute can hold a reference to any object — including an instance of a class you wrote yourself.
The second thing to notice is Car.start(). It does not do the engine work itself. It calls self.engine.start() and wraps the result. That pattern is called delegation: the composite class hands a job to the component instead of doing it. Car uses the engine's behavior without inheriting it.
Knowledge check
Check your understanding
Answer this question before you continue.
What Actually Happens When You Store an Object Inside an Object
Once the example runs, the mental model is simple: an attribute is a name bound to an object, and that object can be another instance of a class you wrote. Nothing more exotic is going on.
Two consequences follow, and both matter.
First, the composite class does not automatically gain the component's methods. Car has no horsepower attribute of its own, and car.start() only works because you wrote the delegation by hand. If you want Car to expose the engine's behavior, you call through the attribute: self.engine.start(). Composition gives you access, not inheritance.
Second, the coupling is loose. If you later rewrite Engine.start() to return something different, Car keeps working as long as the method still exists and still returns a string. Changes to Engine rarely force changes in Car, and changes to Car never reach back into Engine. Compare that to inheritance, where the child class absorbs the parent's interface and implementation whether it wants them or not — every method, every attribute, every future change.
That difference in coupling is the real reason composition shows up so often in production code. Real systems are mostly objects holding other objects, and the ones that survive are the ones where you can change one piece without rewriting the rest.
Knowledge check
Check your understanding
Answer this question before you continue.
Composition vs Inheritance: A Short Comparison
| Inheritance | Composition | |
|---|---|---|
| Relationship modeled | is-a | has-a |
| What gets reused | Interface and implementation | Implementation, through a stored object |
| Coupling | Tighter | Looser |
| Use this when | The is-a sentence is genuinely true | One object owns, contains, or coordinates another |
| Beginner signal you picked wrong | "A Car is an Engine" sounds forced | You are wrapping one method with no real containment |
The one-line rule: say the sentence out loud. "A Car is an Engine" fails. "A Car has an Engine" passes. If the first sentence sounds wrong, store an object instead of inheriting.
Note: Composition is not automatically better than inheritance. It is better when the relationship is genuinely containment or use. When the is-a relationship is real and you want the parent's full interface, inheritance is the clearer choice.
Common Beginner Mistake: Inheriting to Get One Method
Here is the failure mode in its natural habitat. You want Car to reuse Engine.start(), so you write this:
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
def start(self):
return f"Engine with {self.horsepower} hp starting up."
class Car(Engine): # wrong: a car is not an engine
def __init__(self, make, horsepower):
super().__init__(horsepower)
self.make = make
car = Car("Hatchback", 150)
print(car.start())
This runs. It even prints something reasonable. But Car now claims to be a kind of Engine. It inherits horsepower whether or not that belongs on a car, and every future change to Engine — a new method, a renamed attribute, a different constructor — leaks into Car automatically. You borrowed one method and paid for it with the whole class.
The repair is two lines. Store an Engine instance instead, and delegate the call:
class Car:
def __init__(self, make, engine):
self.make = make
self.engine = engine
def start(self):
return f"{self.make}: {self.engine.start()}"
Now Car is a car. It has an engine. When Engine changes, Car only cares if start() still exists.
The diagnostic question to ask yourself: am I inheriting for behavior I want to borrow, or because the two things genuinely are the same kind of thing? Borrowing is composition. Being is inheritance.
When Composition Is the Clearer Choice — and When It Is Not
Use composition when one object owns, contains, or coordinates another. A Report has a Formatter. A Team has a list of Player objects. An Order has a Customer. In each case the sentence passes, and the composite class is genuinely built out of its components.
Use composition when you want to swap the component at runtime. Because the component is just an attribute, you can reassign it: give the same Report a different Formatter object and its output changes without touching the Report class. That flexibility is hard to get from inheritance.
Use inheritance when the is-a relationship is real and you want the parent's full interface, not just one borrowed method. A SavingsAccount is an Account. A Circle is a Shape. Those sentences hold up.
Common mistake: Over-composition. A class that holds ten unrelated components and forwards every call to them is not well-designed — it is a class that has not decided what it is. If you find yourself wrapping everything, the fix is usually to split the class, not to add another wrapper.
Knowledge check
Check your understanding
Answer this question before you continue.
Where This Shows Up in Real Code
You will meet has-a structure constantly once you start looking for it.
In a small automation script, a Report object often holds a Formatter and a DataSource. The report does not format anything itself; it asks the formatter to do it and the data source to supply the rows. Swap either one and the report keeps working.
In a data cleanup script, a Cleaner object typically holds a list of Rule objects and applies each rule in turn. Each rule is a small component with one job. Adding a new rule means adding a new object, not editing the cleaner.
This is also why composition matters for job-readiness. Real codebases are mostly objects holding other objects, and being able to glance at a class and see its has-a structure is a core reading skill. When you can spot containment at a glance, unfamiliar code stops looking like a wall.
Practice: Build a Has-a Relationship Yourself
Time to write one yourself. Create a Battery class with a level attribute and a drain() method, then a Phone class that has a Battery and a use_app() method that drains it.
Expected behavior: calling use_app() three times should reduce the battery level and print the remaining charge.
Hint: store the Battery instance in Phone.__init__, and call self.battery.drain() inside use_app().
class Battery:
def __init__(self, level=100):
self.level = level
def drain(self, amount=10):
self.level -= amount
return self.level
class Phone:
def __init__(self, battery):
self.battery = battery
def use_app(self):
remaining = self.battery.drain()
print(f"Battery at {remaining}%")
battery = Battery()
phone = Phone(battery)
phone.use_app()
phone.use_app()
phone.use_app()
Run it and call use_app() three times. You should see the level drop from 100 to 90, 80, then 70.
Expected output:
Battery at 90%
Battery at 80%
Battery at 70%
Extension: add a second component class called Screen and decide whether Phone should inherit from it or hold it. Justify your choice in one sentence. If you cannot say "a Phone is a Screen" and mean it, you already know the answer.
The Rule to Carry Forward
Before you write class Child(Parent), say the is-a sentence out loud. If it sounds wrong, store an object instead. That single habit will save you from most beginner inheritance mistakes.
Your next step: pick two classes from a script you have already written and check whether they relate by containment. If they do, make sure one holds the other as an attribute rather than inheriting from it. Reading other people's code gets easier the moment you can spot has-a structure at a glance — and writing your own gets cleaner the moment you stop forcing is-a where it does not belong.
Knowledge check
Final check
Finish the article by checking the ideas you just learned.
References
Want a more structured Python path?
Use the Python Starter Pack to turn scattered tutorials into a focused practice path.
Python for Artificial Intelligence Starter Pack
Build a Python foundation you can actually use. The Python for AI Starter Pack brings together a guided path through setup, core programming concepts, data structures, files, JSON, APIs, debugging, and practical projects—so you can move quickly from running your first program to understanding and building useful software.
- 264-page illustrated PDF
- 12 guided Python chapters
- Visual concept diagrams
- Self-assessment quizzes
- Bonus deep-dive sections
- Files, JSON, APIs, debugging & projects
- Foundation for data, automation & AI
Coming soon


