Visual walkthrough — Composition — has-a relationship vs is-a
Before we draw anything, let us agree on the vocabulary so no symbol appears un-earned.
Step 1 — Two responsibilities, two boxes
WHAT. We start with one requirement: a car must be able to start(). Starting really means two separate jobs — "be a car" and "make ignition happen." We draw each job as its own empty box.
WHY. Ignition logic is a self-contained concept that changes on its own schedule (petrol today, electric tomorrow). If we mash it into the car box, then changing ignition means editing the car. Keeping them as separate boxes is the very first move of composition: one box per concept that can change independently.
PICTURE. Two boxes side by side, not yet connected. The Car box knows it needs some ignition, but does not yet know where it lives.

Step 2 — Give the sub-job its own class
WHAT. We turn the right-hand box into a real, usable class. Engine stores one number, horsepower, and offers one behaviour, ignite().
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower # data the engine owns
def ignite(self):
return f"Engine roaring at {self.horsepower}hp"WHY. By isolating the volatile part, the Car will never reach inside the engine — it will only ever call the one public method ignite(). That single method is the entire contract between them. A small contract is a stable contract.
PICTURE. The Engine box now has an inner data slot (horsepower = 120, in yellow) and a public "port" for its method ignite() (in green). The port is the only way in.

horsepower— the yellow slot; it is private data, hidden behind the wall.ignite()— the green port; the only public surface the outside world may touch.
Step 3 — The Car HOLDS an Engine (the "has-a" arrow appears)
WHAT. We give Car a field named engine and let an engine be passed in from outside.
class Car:
def __init__(self, engine): # engine handed in from outside
self.engine = engine # the has-a field: a slot holding the engine box
def start(self):
return "Car: " + self.engine.ignite()WHY — the arrow, term by term. Look at self.engine = engine:
self.engine— a slot inside the Car box. This slot is what "has-a" means physically: the car literally holds a reference to another box.= engine— we fill the slot with a box handed to us from outside. We did not build the engine ourselves; someone gave it to us. (This handing-in is called Dependency Injection and it is what makes swapping possible later.)
PICTURE. The solid blue has-a arrow now leaves the Car's engine slot and lands on the Engine box. This arrow is the composition relationship made visible.

Step 4 — start() delegates through the arrow
WHAT. We trace what happens when someone calls car.start(). The car does not know how to ignite — it forwards the request across the has-a arrow.
WHY. This forwarding is delegation. The car answers "how do I start?" with "I don't; I ask my engine." Term by term in "Car: " + self.engine.ignite():
self.engine— follow the blue arrow to the engine box..ignite()— call the engine's one public port (from Step 2)."Car: " + ...— the car wraps the reply in its own words and hands it back.
PICTURE. A green call arrow curves out of start(), along the has-a link, into ignite(), and a return value flows back. The car never crosses the engine's wall — it only knocks on the green port.

Step 5 — The runtime swap (composition's superpower)
WHAT. While the program is still running, we drop a different engine into the same slot.
c.engine = Engine(300) # pop the old engine out, click a new one in
print(c.start()) # Car: Engine roaring at 300hpWHY. Because the connection is just an arrow to a box, we can re-point the arrow at any moment. c.engine = Engine(300) erases the old target and aims the same self.engine slot at a fresh box. The Car code did not change one character.
PICTURE. The old engine box fades (red, dashed) as the blue has-a arrow re-points to a new engine box. The slot is the same; only its target moved.

Step 6 — Edge case: the empty slot (engine is None)
WHAT. What if the slot is filled with nothing — Car(None)? We must show this case; the reader will hit it eventually.
c = Car(None)
c.start() # BOOM: AttributeError — None has no ignite()WHY. Following the has-a arrow leads to a box that has no ports at all. Calling .ignite() on None fails because there is nothing to delegate to. Composition trades a compile-time guarantee for a runtime slot — so an empty slot is a real, catchable failure mode.
PICTURE. The has-a arrow points into a hollow red "no-box." The green call arrow leaves start() and hits empty space — the crash.

Step 7 — Edge case: two cars sharing one engine (aliasing)
WHAT. A subtle case: what if two car boxes point their slots at the same engine box?
e = Engine(120)
a = Car(e)
b = Car(e) # both slots point to the SAME engine
e.horsepower = 999 # change it once...
# a.start() AND b.start() now both report 999WHY. The slot holds a reference (an arrow), not a copy. Two arrows into one box means editing that box is felt by everyone pointing at it. This is the difference between "shared part" (aggregation) and "owned part" (composition) — see Aggregation vs Composition (UML).
PICTURE. Two has-a arrows from two different Car boxes converge on a single Engine box. A yellow "edit" flash on the shared engine ripples out along both arrows.

The one-picture summary
Everything above collapses into one diagram: the car box, its engine slot, the blue has-a arrow, the green delegation call, and the little "swap" loop that only composition allows — with the two edge cases (empty slot, shared slot) marked in red as the pitfalls to guard.

Recall Feynman: the whole walkthrough in plain words
We wanted a car that can start. Instead of teaching the car how to ignite, we built a separate little machine — the engine — that knows only one trick: ignite(). We gave the car a pocket (the engine field) and slipped the engine machine into it. When you tell the car "start," the car reaches into its pocket, pulls the engine's one lever, and repeats what the engine says. Because the engine sits in a pocket and not welded to the car's body, you can pull it out and drop in a stronger one while the car is still running — that pocket-swap is composition's whole gift. Two warnings: an empty pocket (None) crashes when the car reaches for the lever, and if two cars share one engine in their pockets, tuning that engine tunes both cars at once. Guard the empty pocket with a check; give each car its own engine if you don't want the sharing.
Recall
The blue arrow in every figure points from
The green arrow in the figures represents
Why can composition swap the part at runtime?
What breaks when the engine slot is None?
.ignite() raises AttributeError. Guard with a null check.Why do two cars sharing one engine both see edits?
Connections
- Parent: 2.1.15 Composition — has-a relationship vs is-a (Hinglish)
- Contrast tool: Inheritance — is-a relationship · Liskov Substitution Principle
- The small-surface idea: Encapsulation and Information Hiding
- Handing the part in: Dependency Injection · Strategy Pattern
- Shared vs owned parts: Aggregation vs Composition (UML)
- What inheritance risks: Fragile Base Class Problem