Design Guidelines¶
Choosing between composition and inheritance is one of the most consequential design decisions in object-oriented programming. The widely cited guideline "favor composition over inheritance" exists because composition generally provides greater flexibility, simpler testing, and fewer surprises as a codebase grows. This page summarizes when each approach is appropriate and why.
Mental Model
Start every design decision with the question: "Would I bet that this is-a relationship will hold true in two years?" If the answer is uncertain, use composition -- it is always easier to plug in a new component than to restructure an inheritance tree. Inheritance is a one-way door; composition is a revolving one.
Prefer Composition¶
1. Over Inheritance¶
Using multiple inheritance to combine unrelated capabilities creates rigid class hierarchies that are difficult to modify. Composition lets you attach capabilities dynamically, keeping each component independent and interchangeable.
```python
Fragile: combining unrelated capabilities via inheritance¶
class FlyingDog(Dog, Flying): pass
Flexible: attaching capabilities via composition¶
class Dog: def init(self): self.abilities = []
def add_ability(self, ability):
self.abilities.append(ability)
```
With the composition approach, you can add or remove abilities at runtime without changing the class hierarchy. The inheritance approach, by contrast, locks the set of capabilities into the class definition.
When to Use¶
1. Composition¶
Choose composition when the relationship between objects is best described as "has-a." The following criteria point toward composition.
- Has-a relationship: the object contains or uses another object as a part (e.g., a
Carhas anEngine). - Flexible behavior: the set of capabilities may change at runtime or vary across instances.
- Multiple components: the object is built from several independent parts that can be developed and tested separately.
2. Inheritance¶
Choose inheritance when there is a genuine "is-a" relationship and the subclass truly represents a specialized version of the parent.
- Is-a relationship: the subclass is a natural specialization of the parent (e.g., a
SavingsAccountis aBankAccount). - Shared interface: all subclasses need to expose the same set of methods so that client code can treat them interchangeably.
- Natural hierarchy: the domain has a clear, shallow hierarchy that is unlikely to change frequently.
Ambiguous Cases¶
Real design decisions are often not clear-cut. Some cases genuinely support either approach:
ManagerandEmployee: a manager is-a employee, but a manager also has-a set of responsibilities that could be composed. If roles change at runtime, composition may be better.Stackandlist: a stack is-a restricted list, but inheriting fromlistexposes methods (insert,sort) that violate the stack contract. Composition (wrapping a list) is safer.
When the choice is ambiguous, ask: will this hierarchy need to change? If yes, lean toward composition. If the hierarchy is small and stable, inheritance is fine. See Is-a vs Has-a and Composition vs Inheritance for deeper comparisons.
Quick Decision Tree¶
One-Minute Decision
text
What kind of relationship?
│
├── "Is-a" (true type) ──→ Inheritance
│ └── But will it change at runtime? ──→ Composition
│
├── "Has-a" (containment) ──→ Composition or Aggregation
│ ├── Container creates & owns parts? ──→ Composition
│ └── Parts are shared or pre-existing? ──→ Aggregation
│
└── Unsure? ──→ Default to composition
Summary¶
- Favor composition when you need flexibility, runtime configurability, or when combining unrelated behaviors.
- Use inheritance sparingly and only for genuine is-a relationships with shallow, stable hierarchies.
- When the choice is ambiguous, ask whether the hierarchy will need to change --- if yes, lean toward composition.
- Think about the relationship first --- if "has-a" describes it more accurately than "is-a," composition is almost always the better choice.
Exercises¶
Exercise 1.
Given a scenario where you need to model Logger functionality for different output targets (console, file, network), explain whether inheritance or composition is more appropriate. Implement your chosen approach with a Logger class and interchangeable Output objects.
Solution to Exercise 1
# Composition is more appropriate: Logger "has-a" Output target
class ConsoleOutput:
def write(self, message):
print(f"[CONSOLE] {message}")
class FileOutput:
def __init__(self, filename):
self.filename = filename
def write(self, message):
print(f"[FILE:{self.filename}] {message}")
class NetworkOutput:
def __init__(self, url):
self.url = url
def write(self, message):
print(f"[NETWORK:{self.url}] {message}")
class Logger:
def __init__(self, output):
self._output = output
def log(self, message):
self._output.write(message)
def set_output(self, output):
self._output = output
logger = Logger(ConsoleOutput())
logger.log("Application started")
logger.set_output(FileOutput("app.log"))
logger.log("Switched to file output")
Exercise 2.
You have a Vehicle class with start() and stop() methods. You want to add GPS, AirConditioning, and MusicPlayer features. Implement this using composition, creating separate feature classes. Show how a Vehicle can have any combination of features without creating subclasses for each combination.
Solution to Exercise 2
class GPS:
def status(self):
return "GPS: Active"
class AirConditioning:
def __init__(self, temp=22):
self.temp = temp
def status(self):
return f"AC: {self.temp}C"
class MusicPlayer:
def status(self):
return "Music: Playing"
class Vehicle:
def __init__(self, name, features=None):
self.name = name
self.features = features or []
def start(self):
print(f"{self.name} started")
def stop(self):
print(f"{self.name} stopped")
def show_features(self):
for f in self.features:
print(f" {f.status()}")
basic = Vehicle("Sedan", [GPS()])
luxury = Vehicle("Luxury", [GPS(), AirConditioning(18), MusicPlayer()])
basic.start()
basic.show_features() # GPS: Active
luxury.start()
luxury.show_features() # GPS, AC, Music — no subclasses needed
Exercise 3.
Identify a case where inheritance IS the right choice: model a Shape hierarchy with Circle, Rectangle, and Triangle. Each shape must implement area() and perimeter(). Justify why inheritance is appropriate here (is-a relationship, shared interface, stable hierarchy) and implement it.
Solution to Exercise 3
import math
# Inheritance IS appropriate: Circle is-a Shape, stable hierarchy
class Shape:
def area(self):
raise NotImplementedError
def perimeter(self):
raise NotImplementedError
def describe(self):
return f"{self.__class__.__name__}: area={self.area():.2f}, perimeter={self.perimeter():.2f}"
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
class Triangle(Shape):
def __init__(self, a, b, c):
self.a, self.b, self.c = a, b, c
def area(self):
s = (self.a + self.b + self.c) / 2
return math.sqrt(s * (s - self.a) * (s - self.b) * (s - self.c))
def perimeter(self):
return self.a + self.b + self.c
shapes = [Circle(5), Rectangle(4, 6), Triangle(3, 4, 5)]
for s in shapes:
print(s.describe())
Exercise 4.
A Stack can be modeled either by inheriting from list or by composing a list. Implement both approaches. Then demonstrate the problem with inheritance: show that Stack(list) exposes insert(), sort(), and other methods that violate the stack contract (LIFO only). Explain why composition is the safer choice here.
Solution to Exercise 4
# Inheritance approach — problematic
class StackInherit(list):
def push(self, item):
self.append(item)
def pop_top(self):
return self.pop()
si = StackInherit()
si.push(1)
si.push(2)
si.insert(0, 99) # Violates LIFO! Not blocked.
si.sort() # Also allowed — not a stack operation.
print(si) # [1, 2, 99] — broken stack invariant
# Composition approach — safe
class StackCompose:
def __init__(self):
self._data = []
def push(self, item):
self._data.append(item)
def pop(self):
if not self._data:
raise IndexError("pop from empty stack")
return self._data.pop()
def peek(self):
if not self._data:
raise IndexError("peek at empty stack")
return self._data[-1]
def __len__(self):
return len(self._data)
sc = StackCompose()
sc.push(1)
sc.push(2)
# sc.insert(0, 99) # AttributeError — not exposed
# sc.sort() # AttributeError — not exposed
print(sc.pop()) # 2 — correct LIFO behavior
Inheritance from list exposes the entire list API, including operations that break the stack contract. Composition hides the internal list and only exposes push, pop, peek, and __len__ — exactly the operations a stack should support. This is a textbook case where "is-a" (a stack is-a list) sounds plausible but fails the Liskov Substitution Principle: code that uses list.insert() would break the stack's invariant.
Exercise 5. A colleague says "I always use composition — inheritance is an anti-pattern." Construct a counter-example: describe a scenario where inheritance is clearly simpler and more appropriate than composition. Implement it, and explain the three criteria that make inheritance the right choice in this case.
Solution to Exercise 5
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
def describe(self):
return f"{type(self).__name__}: area = {self.area():.2f}"
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
import math
return math.pi * self.radius ** 2
class Rectangle(Shape):
def __init__(self, w, h):
self.w, self.h = w, h
def area(self):
return self.w * self.h
# Polymorphism: works for any Shape
shapes = [Circle(5), Rectangle(3, 4)]
for s in shapes:
print(s.describe())
Three criteria that make inheritance right here:
- True is-a relationship: "Circle is-a Shape" is natural and stable. Nobody would say "Circle has-a Shape."
- Shared interface for polymorphism: The
describe()method works uniformly across all shapes. Client code (for s in shapes) does not care which specific shape it has. - Shallow, stable hierarchy: The set of shapes rarely changes, and there is only one level of inheritance.
A composition alternative would require every shape to hold a ShapeImpl object and delegate area() to it — more code for no benefit. Inheritance is not an anti-pattern; it is the wrong tool when applied to problems that don't fit the three criteria above.