Skip to content
beginner

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.

Published 2026-10-02Updated 2026-10-049 min read
A tranquil sandy beach with rocky formations at Saint-Malo, Brittany, France.
A tranquil sandy beach with rocky formations at Saint-Malo, Brittany, France. Photo by Efrem Efre on Pexels.

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.

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.

Which design models the relationship between a car and an engine as composition?
Single Choice

Focus: Distinguish an is-a relationship from a has-a relationship when choosing between inheritance and composition.

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 does the example print when `car.start()` is called?
Output Prediction

Focus: Predict the result of calling a composite object's method when it delegates work to its component.

What Actually Happens When You Store an Object Inside an Object

A Car object points to an Engine object through its engine attribute. A start call moves from Car.start() to Engine.start(), showing containment and delegation rather than inheritance.
Composition stores one object inside another and lets the containing object delegate work to it.

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.

A `Car` stores an `Engine` in `self.engine`. Which statement is accurate?
Misconception Check

Focus: Explain that storing a component gives access to it but does not automatically add its methods to the composite object.

Composition vs Inheritance: A Short Comparison

InheritanceComposition
Relationship modeledis-ahas-a
What gets reusedInterface and implementationImplementation, through a stored object
CouplingTighterLooser
Use this whenThe is-a sentence is genuinely trueOne object owns, contains, or coordinates another
Beginner signal you picked wrong"A Car is an Engine" sounds forcedYou 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.

A `Report` should keep its class unchanged while using different formatting behavior at runtime. Which design best matches the article's guidance?
Single Choice

Focus: Identify when composition is useful because a component can be swapped without changing the composite class.

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.

Which situation is the clearest fit for inheritance according to the article?
Question 1 of 2Misconception Check

Focus: Choose inheritance when the is-a relationship is genuine and the child should have the parent's full interface.

In the practice example, `Battery()` starts at 100, and each `phone.use_app()` call drains 10. What are the three printed battery levels after three calls?
Question 2 of 2Output Prediction

Focus: Predict how repeated calls to a composite object's method change the state of its contained component.

References

  1. Inheritance and Composition: A Python OOP Guide – Real Pythonrealpython.com
  2. 7. Inheritance and compositionoxmmscpython.github.io
Practical resource

Want a more structured Python path?

Use the Python Starter Pack to turn scattered tutorials into a focused practice path.

View the bundle
Coming soon

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.

$9
PDF BundlePythonAIBeginner
  • 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

Free Python bundle

Get the LearnPyFast Python for Artificial Intelligence Starter Bundle

Build a Python foundation you can actually use. The Python for Artificial Intelligence 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.

You’ll receive the bundle by email. You can unsubscribe anytime.

No spam. You can unsubscribe anytime. See our Privacy policy.

Related sites

Continue beyond Python

Explore related Worldmonger sites when you want to move from Python basics into JavaScript or LLM application building.

JavaScript tutorialstutorial

LearnJSFast

Beginner-friendly JavaScript tutorials for practical web development and self-taught developers.

JavaScriptFrontendWeb development
Visit LearnJSFast
LLM tutorialstutorial

LearnLLMFast

Practical LLM tutorials for builders who want to understand prompting, workflows, agents, and AI applications.

LLMAIBuilders
Visit LearnLLMFast

Keep learning

Related tutorials

Continue with nearby Python topics and beginner-friendly explanations.