Composition over Inheritance
What Problem Does It Solve?
Deep inheritance hierarchies create rigid, fragile code. A change in a base class can shatter every subclass. A class that inherits behavior it doesn't need (a Duck that can't fly inheriting fly() from Bird) forces awkward overrides. Composition solves this by assembling objects from smaller, interchangeable parts — each part has a single job and can be mixed freely.
Analogy: A pre-built plastic toy car is a single mold — the wheels don't turn, the doors don't open. If the axle breaks, the whole car is trash. LEGO blocks, on the other hand, let you build a car from separate pieces: wheels, chassis, seats, engine. Each piece works independently. You can swap in racing wheels, add a spoiler, or rebuild the car into a plane. Broken axle? Replace it, not the whole car.
How to Identify When to Use It
- Is your inheritance hierarchy more than 2–3 levels deep?
- Do subclasses override or stub out methods inherited from the base?
- Do you have a "god base class" that everything inherits from?
- Are you using inheritance just to share code (not to model an is-a relationship)?
- Would changing one method on the base class require changes in subclasses?
- Do you need to combine behaviors in ways the inheritance hierarchy didn't anticipate?
- Red flag: A subclass passes
*args, **kwargstosuper().__init__()without understanding them.
How to Apply It
- Prefer interfaces/protocols over abstract base classes. Define what an object can do, not what it is.
- Extract varying behavior into separate strategy objects. Pass them in (dependency injection) instead of hard-coding them in subclasses.
- Use mixins or traits sparingly for shared behavior across unrelated classes.
- Favour delegation over inheritance. When a class needs another class's behavior, hold a reference to it rather than inheriting from it.
- Design for testability. Composition makes it trivial to swap real dependencies for mocks/fakes.
# BEFORE: Inheritance gone wrong
from abc import ABC, abstractmethod
class Bird(ABC):
@abstractmethod
def fly(self) -> str:
pass
@abstractmethod
def swim(self) -> str:
pass
@abstractmethod
def quack(self) -> str:
pass
class Duck(Bird):
def fly(self) -> str:
return "Flies"
def swim(self) -> str:
return "Swims"
def quack(self) -> str:
return "Quack!"
class RubberDuck(Bird): # A rubber duck IS-NOT-A bird
def fly(self) -> str:
raise NotImplementedError("Can't fly")
def swim(self) -> str:
return "Floats"
def quack(self) -> str:
return "Squeak!"
# AFTER: Composition with protocols
from typing import Protocol
class FlyBehavior(Protocol):
def fly(self) -> str: ...
class SwimBehavior(Protocol):
def swim(self) -> str: ...
class QuackBehavior(Protocol):
def quack(self) -> str: ...
class CanFly:
def fly(self) -> str:
return "Flies"
class CanSwim:
def swim(self) -> str:
return "Swims"
class CanFloat:
def swim(self) -> str:
return "Floats"
class RealQuack:
def quack(self) -> str:
return "Quack!"
class Squeak:
def quack(self) -> str:
return "Squeak!"
class Duck:
def __init__(self, fly: FlyBehavior, swim: SwimBehavior, quack: QuackBehavior):
self._fly = fly
self._swim = swim
self._quack = quack
def fly(self) -> str:
return self._fly.fly()
def swim(self) -> str:
return self._swim.swim()
def quack(self) -> str:
return self._quack.quack()
# Assemble different kinds of ducks
real_duck = Duck(CanFly(), CanSwim(), RealQuack())
rubber_duck = Duck(CanFly(), CanFloat(), Squeak()) # Rubber duck doesn't fly but we can still compose itReal-World Example
Before — A reporting system using inheritance, where every report type extends a base:
from abc import ABC, abstractmethod
class Report(ABC):
def generate(self) -> str:
data = self.fetch_data()
formatted = self.format(data)
return self.render(formatted)
@abstractmethod
def fetch_data(self) -> list[dict]:
pass
@abstractmethod
def format(self, data: list[dict]) -> str:
pass
@abstractmethod
def render(self, content: str) -> str:
pass
class PdfSalesReport(Report):
def fetch_data(self) -> list[dict]:
return query_sales_db()
def format(self, data: list[dict]) -> str:
return json.dumps(data)
def render(self, content: str) -> str:
return f"<pdf>{content}</pdf>"
class HtmlSalesReport(Report):
def fetch_data(self) -> list[dict]:
return query_sales_db()
def format(self, data: list[dict]) -> str:
return json.dumps(data)
def render(self, content: str) -> str:
return f"<html>{content}</html>"After — Compose each responsibility independently:
from typing import Protocol
class DataFetcher(Protocol):
def fetch(self) -> list[dict]: ...
class Formatter(Protocol):
def format(self, data: list[dict]) -> str: ...
class Renderer(Protocol):
def render(self, content: str) -> str: ...
class SalesDbFetcher:
def fetch(self) -> list[dict]:
return query_sales_db()
class JsonFormatter:
def format(self, data: list[dict]) -> str:
return json.dumps(data)
class PdfRenderer:
def render(self, content: str) -> str:
return f"<pdf>{content}</pdf>"
class HtmlRenderer:
def render(self, content: str) -> str:
return f"<html>{content}</html>"
class Report:
def __init__(self, fetcher: DataFetcher, formatter: Formatter, renderer: Renderer):
self._fetcher = fetcher
self._formatter = formatter
self._renderer = renderer
def generate(self) -> str:
data = self._fetcher.fetch()
formatted = self._formatter.format(data)
return self._renderer.render(formatted)
# Assemble any combination
pdf_report = Report(SalesDbFetcher(), JsonFormatter(), PdfRenderer())
html_report = Report(SalesDbFetcher(), JsonFormatter(), HtmlRenderer())
# Need CSV instead? Write a CsvFormatter — no inheritance changes needed.
# Need a new data source? Write a new fetcher.Common Mistakes / Pitfalls
- Composition for everything. Inheritance isn't evil — it's appropriate for true is-a hierarchies (e.g.,
CircleextendsShape). Use composition when behavior varies independently of identity. - Over-composing. Too many tiny objects can make the system hard to reason about. Group fine-grained behaviors into meaningful components.
- The "has-a" trap. Composition models "has-a", not "depends-on". Passing a dependency via constructor is composition; passing it as a function parameter is just a dependency.
- Losing polymorphic benefits. Inheritance gives you automatic subtype polymorphism (
List<Shape>works withCircle). With composition you need explicit interfaces or protocols to achieve the same. - Complex wiring. Composition shifts complexity from class hierarchies to object wiring. Use dependency injection frameworks or factories to manage assembly.
