What is it? #
Both reuse behaviour. Inheritance says a class is a kind of another. Composition says a class has parts it delegates to.
The usual advice — favour composition over inheritance — is good but incomplete. Inheritance is genuinely right when types share identity and are substitutable for one another.
The strongest argument for composition is combinatorics. When behaviours vary along more than one axis, inheritance needs a class for every combination while composition needs one part per option.
The strongest argument for inheritance is a stable, shallow hierarchy where every subclass truly can stand in for the parent.
Think of it like this #
Coffee. You could define classes for Latte, IcedLatte, LargeIcedLatte, LargeIcedDecafLatte — every combination its own class. Three sizes, two temperatures, two caffeine options and four bases give forty-eight classes.
Or you could treat a drink as a base plus a size plus a temperature plus a caffeine choice, and assemble it. That is composition, and it is why real menus are built that way.
Simple example #
A notification system where messages vary by channel (email, SMS, push), formatting (plain, HTML, markdown) and delivery timing (immediate, scheduled, batched). Inheritance would need a class per combination; composition needs three small families.
Code #
# --- The inheritance approach collapses under combinations ---
# class EmailNotification(Notification): ...
# class HtmlEmailNotification(EmailNotification): ...
# class ScheduledHtmlEmailNotification(HtmlEmailNotification): ...
# class BatchedScheduledHtmlEmailNotification(...): ...
# 3 channels x 3 formats x 3 timings = 27 classes
# --- Composition: three small families, assembled ---
class EmailChannel:
def deliver(self, to, body): print(f"[email -> {to}] {body}")
class SmsChannel:
def deliver(self, to, body): print(f"[sms -> {to}] {body[:160]}")
class PlainFormatter:
def format(self, subject, text): return f"{subject}: {text}"
class HtmlFormatter:
def format(self, subject, text): return f"<h1>{subject}</h1><p>{text}</p>"
class ImmediateSchedule:
def when(self): return "now"
class NightlyBatch:
def when(self): return "23:00"
class Notification:
def __init__(self, channel, formatter, schedule):
self.channel, self.formatter, self.schedule = channel, formatter, schedule
def send(self, to, subject, text):
body = self.formatter.format(subject, text)
print(f"(scheduled {self.schedule.when()})", end=" ")
self.channel.deliver(to, body)
Notification(EmailChannel(), HtmlFormatter(), ImmediateSchedule()).send(
"[email protected]", "Order shipped", "Your order is on its way."
)
Notification(SmsChannel(), PlainFormatter(), NightlyBatch()).send(
"+910000000000", "Reminder", "Payment due tomorrow."
)
# 3 + 2 + 2 = 7 small classes cover every combination
# --- Where inheritance is still the right answer ---
class HttpError(Exception):
status = 500
class NotFound(HttpError):
status = 404
class Forbidden(HttpError):
status = 403
# A NotFound genuinely IS an HttpError, the hierarchy is shallow and stable,
# and catching HttpError correctly catches all of them.
Decision guide
Use inheritance when
- the child genuinely IS a kind of the parent
- every child can replace the parent without surprises
- the hierarchy is shallow and the base is stable
- a framework explicitly expects you to subclass
Use composition when
- behaviours vary along more than one axis
- you want to swap behaviour at runtime or per environment
- you only want to reuse a method, not adopt an identity
- you want each part testable on its own
How it works #
The commented block at the top shows the class explosion. Every new option multiplies the number of classes, and each one is a place where a bug can hide.
The composition version has one small class per option. Adding a push channel means one new class and it instantly works with every formatter and schedule.
Notification coordinates the parts. It knows the order of operations — format, then schedule, then deliver — and nothing about how each step works.
The exception hierarchy at the bottom is inheritance used well. NotFound really is an HttpError; the hierarchy is one level deep; and except HttpError catching all subtypes is exactly the behaviour you want. Composition here would be worse.
When you realise you chose inheritance wrongly, the refactor is mechanical. Take the method that varies between subclasses, move it into its own small class, give the original a field holding one of those, and delegate. The subclasses then disappear one by one.
The honest summary: inheritance for identity, composition for behaviour.
Real-world use #
Framework code uses inheritance because it is designed for it — base views, base models, base commands. Application code leans on composition because requirements vary in ways nobody predicted when the base class was written.
Exception hierarchies, UI component trees and abstract syntax tree node types are all reasonable inheritance uses: the "is a" relationship is genuine and the substitutability is exactly what callers want.
Anything policy-shaped — pricing, retries, formatting, routing, permissions — is better composed. These change frequently and independently, and burying them in a hierarchy makes each change risky.
Teams that have maintained a deep hierarchy for a few years tend to arrive at the same conclusion: shallow inheritance where the relationship is real, composition everywhere else.
Common mistakes #
- Using inheritance to reuse a single method, adopting an identity you did not want.
- Building a class per combination of options instead of composing parts.
- Applying "always use composition" so strictly that natural hierarchies get contorted.
- Deep hierarchies where finding which class defines a method takes a search.
- Refactoring a working shallow hierarchy with no actual problem to solve.
Practice #
Take a hierarchy of Employee, SalariedEmployee, HourlyEmployee, ContractEmployee where each also varies by payment method and tax region. Count how many classes a pure inheritance model needs. Then redesign it with composition and count again. Write two sentences on which you would ship.