What's the difference between composition and inheritance, and when do you prefer one over the other?
Quick Answer
**Inheritance** models an "is-a" relationship and reuses behavior by subclassing; **composition** models a "has-a" relationship and reuses behavior by holding a reference to another object and delegating to it. Composition is generally preferred because it avoids tight coupling to a base class's implementation and stays flexible when requirements change — the common guideline is "favor composition over inheritance" and reserve inheritance for genuine is-a relationships with stable, well-designed base classes.
Detailed Answer
Inheritance: is-a
class Vehicle:
def start_engine(self):
return "engine started"
class Car(Vehicle): # a Car IS-A Vehicle
def honk(self):
return "beep!"
Car automatically gets everything Vehicle provides. This works well
when the relationship is genuinely "is-a" and stable — a Car really is a
kind of Vehicle, and that's unlikely to change.
Composition: has-a
class Engine:
def start(self):
return "engine started"
class Car: # a Car HAS-A Engine (delegates to it)
def __init__(self):
self.engine = Engine()
def start_engine(self):
return self.engine.start()
Car doesn't inherit Engine's behavior — it holds an Engine instance
and delegates to it. This decouples Car from Engine's implementation:
swapping in an ElectricEngine just means constructing a different object,
with no class-hierarchy changes.
Why composition is usually favored
- Looser coupling: a subclass inherits everything from its parent, including internals you may not want or that may change unexpectedly; composition only exposes what you explicitly delegate to.
- Flexibility at runtime: you can swap a composed object
(
car.engine = ElectricEngine()) without touching the class definition; swapping inherited behavior requires changing the class hierarchy. - Avoids fragile base class problems: changes to a deeply inherited base class can silently break subclasses several levels down; composed objects have an explicit, narrower contract.
- Sidesteps multiple-inheritance/MRO complexity: composing several collaborator objects avoids diamond-inheritance and MRO conflicts entirely.
When inheritance is still the right call
- The relationship is genuinely "is-a" and unlikely to need swapping
(e.g.,
class JSONDecodeError(ValueError)— a JSON decode error really is a kind of value error, permanently). - You want to reuse a stable, well-tested base class's behavior via
polymorphism (framework base classes like Django's
Model, or ABCs meant specifically to be subclassed). - Mixins for adding small, orthogonal, well-isolated behavior
(
LoggingMixin,TimestampMixin) — this is inheritance used in a composition-like, capability-additive way.
Interview-ready summary: Inheritance reuses behavior through an is-a relationship and a class hierarchy; composition reuses behavior through a has-a relationship and object delegation. Composition is usually the safer default because it's more loosely coupled and flexible — reserve inheritance for genuinely stable is-a relationships or narrow, well-scoped mixins.