Inheritance vs Composition
Is-a versus has-a — when to extend a class, and why most designs are better off assembling objects from parts.
Big idea
Inheritance says "a B is a kind of A" and hands B everything A has. Composition says "a B has an A" and lets B use it through its interface. Both reuse code, but inheritance locks the relationship in at compile time while composition lets you swap parts at runtime — which is why "favour composition over inheritance" became the default advice.
The problem it solves
You build a game. Character has move(). Wizard(Character) adds cast(). Archer(Character) adds shoot(). Then design asks for a wizard who can also shoot. Options:
WizardArcher(Wizard)and copyshoot()in — duplication.- Push
shoot()up intoCharacter— now every character can shoot, including the shopkeeper. - Multiple inheritance — and the diamond of method lookups that comes with it.
Every new combination multiplies subclasses. The tree was the wrong shape for "abilities that mix and match".
Think of it like
A phone case versus a phone. You don't build a "phone-that-is-waterproof" by manufacturing a new species of phone; you put the phone in a waterproof case, and tomorrow you can swap it for a wallet case. Inheritance is genetics — you're born with it. Composition is equipment — you pick it up and put it down.
Side by side
On the left, every combination needs a new class. On the right, Hero holds a list of abilities, and a wizard-archer is just a hero holding both.
In code
class Fireball:
name = "fireball"
def use(self):
return "hurls a fireball"
class Arrow:
name = "arrow"
def use(self):
return "looses an arrow"
class Heal:
name = "heal"
def use(self):
return "heals an ally"
class Hero:
def __init__(self, title, *abilities):
self.title = title
self._abilities = {a.name: a for a in abilities} # has-a, not is-a
def learn(self, ability): # change at runtime
self._abilities[ability.name] = ability
def use(self, name):
ability = self._abilities.get(name)
return f"{self.title} {ability.use()}" if ability else f"{self.title} can't {name}"
mage = Hero("Mage", Fireball())
ranger = Hero("Ranger", Arrow())
spellbow = Hero("Spellbow", Fireball(), Arrow()) # no new class needed
print(mage.use("fireball"))
print(ranger.use("fireball"))
print(spellbow.use("arrow"))
ranger.learn(Heal())
print(ranger.use("heal"))output
Mage hurls a fireball
Ranger can't fireball
Spellbow looses an arrow
Ranger heals an allyRun and edit it in the app.
When inheritance is the right tool
Inheritance is still the right call when the relationship is a true, stable "is-a" and the subclass honours every promise of the parent (Liskov Substitution):
- A framework hands you a base class to fill in (a test case, a UI widget) — the Template Method shape.
- A small, closed family that genuinely shares most behaviour (
Circle,SquareunderShape).
Reach for composition when behaviour varies independently along several axes, when you want to change behaviour at runtime, or when you only want some of a parent's methods (inheriting the rest is how a Stack(list) ends up with insert(0, x)).
A Car needs an Engine, and you want to swap petrol for electric engines later. Which relationship fits?
The fragile base class
Inheritance couples the child to the parent's implementation, not just its interface. Change how the parent's methods call each other and a subclass that overrode one of them can silently break.
class Bag:
def __init__(self):
self.items = []
def add(self, x):
self.items.append(x)
def add_all(self, xs):
for x in xs:
self.add(x) # the parent happens to reuse add()
class CountingBag(Bag):
def __init__(self):
super().__init__()
self.count = 0
def add(self, x):
self.count += 1
super().add(x)
def add_all(self, xs):
self.count += len(xs)
super().add_all(xs)
b = CountingBag()
b.add_all(["a", "b", "c"])
print(b.count)What does it print? Show the answer
6Run and edit it in the app.
The child counted each item twice because the parent's add_all calls add. Nothing in Bag's interface says so — the child depended on an implementation detail. A CountingBag that wraps a Bag can't fall into this.
Watch it
Pros and cons
| Inheritance | Composition | |
|---|---|---|
| Reuse | Automatic — you get everything | Explicit — you forward what you need |
| Flexibility | Fixed when the class is written | Parts can change at runtime |
| Coupling | To the parent's implementation | Only to the part's interface |
| Best for | Stable is-a families, frameworks | Mix-and-match behaviour |
How it relates
- Strategy, Decorator, Bridge, and State are all "use composition instead of a subclass explosion".
- The refactoring Replace Inheritance with Delegation (Dealing with Generalization) mechanically moves you from left to right.
- Liskov Substitution tells you when an inheritance relationship is actually sound.
What is the "fragile base class" problem?
Key terms
- Inheritance — A subclass reuses and extends a parent class; models "is-a".
- Composition — An object holds other objects and delegates work to them; models "has-a".
- Delegation — Forwarding a request to a contained object instead of handling it yourself.
- Fragile base class — When internal changes to a parent class break subclasses that depended on them.