Composition over Inheritance
Composition builds an object from other objects it owns, instead of extending a class it inherits. Where inheritance says "a SavingsAccount is a BankAccount," composition says "a Report has a DataSource and a Formatter." The has-a relation is the more common real-world shape, and it avoids the two costs of inheritance: the rigid is-a commitment (a subclass can override but not remove what it inherits) and the coupling to a parent's internals. This chapter builds the pattern - delegation, the strategy object, and the facade - and gives the decision rule for when to reach for which.
Delegation: The Core Move
class Logger:
def __init__(self, prefix: str):
self.prefix = prefix
self.lines: list[str] = []
def write(self, msg: str) -> None:
self.lines.append(f"[{self.prefix}] {msg}")
class Service:
"""Composes a Logger; does not inherit it."""
def __init__(self, name: str):
self.name = name
self._log = Logger(name) # has-a: an owned component
def start(self) -> None:
self._log.write("started")
def stop(self) -> None:
self._log.write("stopped")
def log_lines(self) -> list[str]:
return self._log.lines # delegate the query outward
s = Service("billing")
s.start()
s.stop()
print(s.log_lines())
# ['[billing] started', '[billing] stopped']
The Service owns a Logger as a plain attribute and forwards the calls it wants to expose. The contrast with inheritance is sharp: inheriting a LoggingMixin would flatten the logger's methods into Service's namespace and bind self to the service (so the mixin's self.lines becomes the service's lines); composition keeps the logger a distinct object with its own state, and the service chooses exactly which calls to forward. The forwarding methods are the delegation - the service is the public face, the logger is the mechanism. The payoff is swap-ability: change the logger's class (to one that writes to a file) and the service is untouched, because it depends on the interface it calls, not on a base class it cannot shed.
The Strategy Object: Behavior as a Component
class PricingStrategy:
def price(self, base: float, qty: int) -> float:
raise NotImplementedError
class BulkDiscount(PricingStrategy):
def price(self, base: float, qty: int) -> float:
return base * qty * (0.9 if qty >= 100 else 1.0)
class LadderPrice(PricingStrategy):
def price(self, base: float, qty: int) -> float:
return base * qty * max(0.5, 1.0 - 0.01 * (qty // 50))
class Order:
def __init__(self, strategy: PricingStrategy):
self._strategy = strategy # the behavior is a component
def total(self, base: float, qty: int) -> float:
return self._strategy.price(base, qty)
# the SAME Order, two different behaviors - no subclassing
a = Order(BulkDiscount())
b = Order(LadderPrice())
print(a.total(10, 150)) # 1350.0
print(b.total(10, 150)) # 700.0
The strategy pattern is composition's flagship: the variable behavior (pricing) is its own object, and the host (Order) holds one and delegates to it. Switching strategies is a construction-time choice - or a runtime one if you make strategy settable - with no class hierarchy and no if isinstance ladder. The contrast with the inheritance alternative is the design test: "pricing" is not a kind of Order; an Order that is a BulkDiscountOrder and a LadderOrder is two subclasses differing in one method, which is the exact shape that should be a strategy. The duck-typing pays off - Order does not need to name PricingStrategy; anything with a price(base, qty) works, and a typing.Protocol from the polymorphism chapter makes that contract explicit.
The Facade: One Object, Many Components
class Connection:
def open(self) -> None: pass
def close(self) -> None: pass
class QueryBuilder:
def build(self, table: str, where: str) -> str:
return f"SELECT * FROM {table} WHERE {where}"
class Cache:
def __init__(self) -> None:
self._store: dict[str, str] = {}
def get(self, k: str) -> str | None:
return self._store.get(k)
def set(self, k: str, v: str) -> None:
self._store[k] = v
class Repo:
"""A facade: composes connection + builder + cache behind one API."""
def __init__(self) -> None:
self._conn = Connection()
self._qb = QueryBuilder()
self._cache = Cache()
def fetch(self, table: str, where: str) -> str:
key = f"{table}:{where}"
hit = self._cache.get(key)
if hit is not None:
return hit
self._conn.open()
sql = self._qb.build(table, where)
result = f"rows({sql})"
self._conn.close()
self._cache.set(key, result)
return result
r = Repo()
print(r.fetch("users", "active=1")) # rows(SELECT ...)
print(r.fetch("users", "active=1")) # served from the cache
The facade composes several collaborators and exposes a simpler surface than their sum. The components keep their own state and responsibilities; the facade orchestrates (open, build, query, cache) and hides the wiring from callers. This is the shape of most real "service" classes - they are not a hierarchy, they are a graph of owned components with one public entry point. The discipline: the facade owns the lifecycle (it opens and closes the connection, so callers cannot forget), and it does not leak the components' types into its public API unless a caller genuinely needs them. Composition scales to this many-part graph trivially; the inheritance alternative - a class that is a Connection and a QueryBuilder and a Cache - is the multiple-inheritance mess the MRO chapter warned about.
The Decision Rule
| Signal | Reach for |
|---|---|
| "A child is genuinely a kind of the parent," and consumers use it as such | inheritance |
| "This object needs a capability it does not itself define" | composition (delegate to a component) |
| "The behavior varies, and it is not a type distinction" | strategy object (composition with a delegate) |
| "Many collaborators, one simple entry point" | facade (composition as orchestration) |
| "I need to share a few methods, not a whole type" | mixin or a helper object, not a base class |
The one-sentence version: inherit when the relation is a true is-a that the type system should express; compose when the relation is a has-a or a uses-a. The pragmatic tiebreaker - when in doubt, compose - works because composition is always available (you can always own a component) while inheritance is a commitment (you inherit everything, and the parent's future changes reach you). The cost of composition is a few forwarding lines and a slightly longer object graph; the cost of a wrong inheritance is a hierarchy you spend quarters unwinding.
Summary
Composition builds from owned components: self._log = Logger(...) plus forwarding methods is delegation, a strategy attribute whose methods the host calls is the strategy pattern (variable behavior as a swappable component), and a facade is composition used as orchestration - one public object over many collaborators, owning their lifecycle. The contrast with inheritance is the design: has-a relations are swappable and decoupled (change the component's class, the host is untouched), while is-a is a permanent commitment to a parent's full namespace. Use inheritance for genuine kinds the type system should name; use composition for capabilities, variable behavior, and multi-part objects - and when the two feel equally plausible, compose, because the forwarding lines are cheap and the wrong base class is not.