Skip to content
FollowtheDamnCode

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 copy shoot() in — duplication.
  • Push shoot() up into Character — 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

The same abilities, inherited versus composed

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

Abilities as parts, not ancestors
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 ally

Run 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, Square under Shape).

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.

Predict: a subclass that counts additions
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
6

Run 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

InheritanceComposition
ReuseAutomatic — you get everythingExplicit — you forward what you need
FlexibilityFixed when the class is writtenParts can change at runtime
CouplingTo the parent's implementationOnly to the part's interface
Best forStable is-a families, frameworksMix-and-match behaviour

How it relates

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.