Why prefer composition over inheritance?
Answer
Flexibility and change
- Composition is “has-a”: the object holds references to other objects (dependencies). You can swap or add behaviors by changing what you inject, without touching the type hierarchy. Inheritance is “is-a”: the subclass is tied to one base; changing behavior often means new subclasses or deeper hierarchies, which get rigid and brittle.
Single responsibility and smaller types
- A class that composes dependencies does one job and delegates the rest. With inheritance, base classes tend to grow (every subclass adds a “variant”), and you get fat base classes, fragile overrides, and unexpected behavior when multiple levels override the same method.
Testing
- With composition, you inject mocks or stubs (e.g. a fake repository, a no-op logger). With inheritance, you often need to subclass or patch to isolate behavior, which is harder and more coupled.
Avoiding inheritance pitfalls
- Deep inheritance leads to fragile base classes, the “diamond” problem when multiple inheritance is used, and unclear overrides (which
super()is which?). Composition avoids these by keeping relationships explicit and one-directional.
When inheritance still makes sense
- Use inheritance when you have a true subtype and a stable, small hierarchy (e.g. a few specialized handlers that share a clear interface). Prefer composition for “using” another capability (logging, storage, transport); prefer inheritance for “is a kind of” with a stable contract.
In code
Same job done two ways — a service that processes data and reports what it did.
Inheritance bakes the behavior into the hierarchy; to change how it reports, you subclass:
class Service:
def process(self, data):
result = data.upper()
self.report(f"processed {data!r}")
return result
def report(self, msg):
print(msg) # console, hard-coded
class FileService(Service): # subclass just to change reporting
def report(self, msg):
with open("svc.log", "a") as f:
f.write(msg + "\n")
Add Slack alerts and a JSON format and you need a subclass per combination — the combinatorial explosion. Reporting can’t change at runtime, and tests must subclass to silence output.
Composition injects the behavior — the service has-a reporter and delegates:
class ConsoleReporter:
def report(self, msg):
print(msg)
class FileReporter:
def __init__(self, path):
self.path = path
def report(self, msg):
with open(self.path, "a") as f:
f.write(msg + "\n")
class Service:
def __init__(self, reporter): # inject the capability
self.reporter = reporter
def process(self, data):
result = data.upper()
self.reporter.report(f"processed {data!r}")
return result
Service(ConsoleReporter()) # swap behavior, no new subclass
Service(FileReporter("svc.log"))
# tests: Service(FakeReporter()) — capture messages and assert on them
One Service; behaviors combine by passing different objects — swappable at runtime, trivially tested with a fake reporter, and no hierarchy to grow.
Inheritance done right is a true is-a with a stable contract:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return 3.14159 * self.r ** 2
class Square(Shape):
def __init__(self, s):
self.s = s
def area(self):
return self.s ** 2
A Circle genuinely is a Shape; the hierarchy is shallow and subclasses only implement the contract — nothing to override later. That’s where inheritance fits.
Summary
- Prefer composition to get flexible, testable, and maintainable code; use inheritance sparingly for clear “is-a” relationships and stable hierarchies.
Interview angle
- “Why prefer composition?” - inheritance couples you to a base class’s entire surface and its future changes, and it’s fixed at class-definition time. Composition lets you assemble behaviour from parts and change it at runtime, and it keeps each part independently testable.
- “When is inheritance still right?” - genuine is-a substitutability, where a subclass can be used anywhere the base can (Liskov). Framework extension points and abstract base classes defining a contract are legitimate uses.
- “What’s the smell that you inherited for the wrong reason?” - a subclass that overrides a method to raise
NotImplementedError, or one that uses only a fraction of the base class. Both mean you wanted reuse, not substitutability. - “How does this look in Python specifically?” - protocols plus injected collaborators rather than deep hierarchies. Mixins are the middle ground and are fine in moderation; a class with five mixins and a confusing MRO is the failure mode.