Skip to content

Composition vs Inheritance

Mental Model

Inheritance says "I am a kind of X" and locks you into X's structure forever. Composition says "I use an X" and lets you swap X for Y at any time. Choose inheritance when the relationship is truly taxonomic (a Dog is an Animal); choose composition when you are assembling capabilities (a Dog has a Bark).

Core Differences

1. Relationship Type

Inheritance models "is-a" relationships: ```python class Animal: def speak(self): pass

class Dog(Animal): # Dog IS-A Animal def speak(self): return "Woof" ```

Composition models "has-a" relationships: ```python class Bark: def sound(self): return "Woof"

class Dog: def init(self): self.bark = Bark() # Dog HAS-A Bark

def speak(self):
    return self.bark.sound()

```

2. Coupling Strength

Aspect Inheritance Composition
Coupling Tight Loose
Dependency Compile-time Runtime
Flexibility Low High
Changes impact Cascading Isolated

3. Code Reuse Mechanism

Inheritance reuses through extension: ```python class Vehicle: def move(self): return "Moving"

class Car(Vehicle): pass # Inherits move() ```

Composition reuses through delegation: ```python class Movement: def move(self): return "Moving"

class Car: def init(self): self.movement = Movement()

def move(self):
    return self.movement.move()

```

When to Use Each

1. Use Inheritance When

Clear hierarchies exist: ```python class Shape: def area(self): pass

class Circle(Shape): def init(self, radius): self.radius = radius

def area(self):
    return 3.14159 * self.radius ** 2

class Rectangle(Shape): def init(self, width, height): self.width = width self.height = height

def area(self):
    return self.width * self.height

```

Polymorphism is needed: ```python def calculate_total_area(shapes: list[Shape]): return sum(shape.area() for shape in shapes)

shapes = [Circle(5), Rectangle(4, 6)] print(calculate_total_area(shapes)) ```

Shared interface required: ```python class Drawable: def draw(self): pass

class Button(Drawable): def draw(self): return "Drawing button"

class Image(Drawable): def draw(self): return "Drawing image" ```

2. Use Composition When

Building from parts: ```python class Engine: def start(self): return "Engine started"

class Wheels: def rotate(self): return "Wheels rotating"

class Car: def init(self): self.engine = Engine() self.wheels = Wheels()

def drive(self):
    return f"{self.engine.start()}, {self.wheels.rotate()}"

```

Runtime flexibility needed: ```python class FileLogger: def log(self, msg): return f"File: {msg}"

class ConsoleLogger: def log(self, msg): return f"Console: {msg}"

class Application: def init(self, logger): self.logger = logger # Flexible at runtime

def run(self):
    self.logger.log("App started")

Switch logger at runtime

app = Application(FileLogger()) app.logger = ConsoleLogger() ```

Avoiding deep hierarchies: ```python

❌ BAD - Deep inheritance

class Entity: pass

class LivingEntity(Entity): pass

class Animal(LivingEntity): pass

class Mammal(Animal): pass

class Dog(Mammal): pass

✅ GOOD - Composition

class Entity: def init(self, behaviors): self.behaviors = behaviors

class Dog(Entity): def init(self): super().init([ LivingBehavior(), AnimalBehavior(), MammalBehavior() ]) ```

Problems with Inheritance

1. Fragile Base Class

Changes to parent break children:

```python class Animal: def move(self): return "Walking"

class Bird(Animal): def fly(self): return "Flying"

Later, Animal changes

class Animal: def move(self, speed): # Added parameter return f"Walking at {speed}"

Bird breaks!

bird = Bird() bird.move() # ❌ TypeError: missing 1 required positional argument ```

With composition: ```python class WalkingBehavior: def move(self): return "Walking"

class FlyingBehavior: def move(self): return "Flying"

class Bird: def init(self): self.movement = FlyingBehavior()

def move(self):
    return self.movement.move()

Isolated change

class WalkingBehavior: def move(self, speed=1): # Bird unaffected return f"Walking at {speed}" ```

2. Inflexible Hierarchy

Can't change behavior after instantiation:

```python class Animal: def speak(self): return "Some sound"

class Dog(Animal): def speak(self): return "Woof"

Can't make dog meow

dog = Dog()

Stuck with "Woof"

```

With composition: ```python class Bark: def sound(self): return "Woof"

class Meow: def sound(self): return "Meow"

class Animal: def init(self, sound_maker): self.sound_maker = sound_maker

def speak(self):
    return self.sound_maker.sound()

animal = Animal(Bark()) animal.speak() # "Woof"

Can change behavior

animal.sound_maker = Meow() animal.speak() # "Meow" ```

3. Multiple Inheritance Issues

Diamond problem:

```python class A: def method(self): return "A"

class B(A): def method(self): return "B"

class C(A): def method(self): return "C"

class D(B, C): pass

d = D() d.method() # Which method? Depends on MRO ```

Composition avoids this: ```python class A: def method(self): return "A"

class B: def method(self): return "B"

class C: def method(self): return "C"

class D: def init(self): self.b = B() self.c = C()

def b_method(self):
    return self.b.method()

def c_method(self):
    return self.c.method()

Clear and explicit

d = D() d.b_method() # "B" d.c_method() # "C" ```

Advantages of Composition

1. Better Encapsulation

Hide implementation:

```python class EmailValidator: def validate(self, email): return "@" in email

class UserService: def init(self): self._validator = EmailValidator() # Private

def create_user(self, email):
    if self._validator.validate(email):
        return f"User created: {email}"
    return "Invalid email"

Client doesn't know about validator

service = UserService() service.create_user("alice@example.com") ```

2. Runtime Flexibility

Change behavior dynamically:

```python class SortStrategy: def sort(self, data): pass

class QuickSort(SortStrategy): def sort(self, data): return sorted(data)

class BubbleSort(SortStrategy): def sort(self, data): return sorted(data)

class Sorter: def init(self): self.strategy = QuickSort()

def set_strategy(self, strategy):
    self.strategy = strategy

def sort(self, data):
    return self.strategy.sort(data)

sorter = Sorter() sorter.sort([3, 1, 2])

Switch at runtime

sorter.set_strategy(BubbleSort()) ```

3. Easier Testing

Inject mock objects:

```python class MockDatabase: def query(self, sql): return [{"id": 1, "name": "Test"}]

class UserRepository: def init(self, database): self.database = database

def get_users(self):
    return self.database.query("SELECT * FROM users")

Easy testing

repo = UserRepository(MockDatabase()) users = repo.get_users() assert len(users) == 1 ```

Combining Both

1. Interface Inheritance + Composition

Best of both worlds:

```python from abc import ABC, abstractmethod

class PaymentMethod(ABC): @abstractmethod def process(self, amount): pass

class CreditCardGateway: def charge(self, amount): return f"Charged ${amount} to credit card"

class CreditCardPayment(PaymentMethod): def init(self): self.gateway = CreditCardGateway() # Composition

def process(self, amount):
    return self.gateway.charge(amount)

class PayPalGateway: def send_payment(self, amount): return f"Sent ${amount} via PayPal"

class PayPalPayment(PaymentMethod): def init(self): self.gateway = PayPalGateway() # Composition

def process(self, amount):
    return self.gateway.send_payment(amount)

Polymorphism through inheritance

Implementation through composition

def process_payment(method: PaymentMethod, amount): return method.process(amount)

process_payment(CreditCardPayment(), 100) process_payment(PayPalPayment(), 50) ```

2. Template Method Pattern

```python class Algorithm(ABC): def execute(self): self.step1() self.step2() self.step3()

@abstractmethod
def step1(self):
    pass

@abstractmethod
def step2(self):
    pass

@abstractmethod
def step3(self):
    pass

class ConcreteAlgorithm(Algorithm): def init(self, helper): self.helper = helper # Composition

def step1(self):
    return self.helper.do_something()

def step2(self):
    return "Step 2"

def step3(self):
    return "Step 3"

```

3. Strategy with Inheritance

```python class Strategy(ABC): @abstractmethod def execute(self): pass

class ConcreteStrategyA(Strategy): def execute(self): return "Strategy A"

class ConcreteStrategyB(Strategy): def execute(self): return "Strategy B"

class Context: def init(self, strategy: Strategy): self.strategy = strategy # Composition

def do_work(self):
    return self.strategy.execute()

```

Design Guidelines

1. Prefer Composition

Modern best practice:

"Favor composition over inheritance"

```python

Instead of:

class FlyingAnimal(Animal): pass

class SwimmingAnimal(Animal): pass

class FlyingSwimmingAnimal(FlyingAnimal, SwimmingAnimal): pass

Use:

class Animal: def init(self, abilities): self.abilities = abilities

def perform(self):
    for ability in self.abilities:
        ability.execute()

duck = Animal([FlyAbility(), SwimAbility()]) ```

2. When Inheritance is OK (and Sometimes Simpler)

Composition is not always simpler. For small, stable hierarchies inheritance is often less code and easier to read. Don't force composition when a two-level hierarchy with three subclasses would do the job.

Use inheritance for:

  • True is-a relationships
  • Abstract base classes (interfaces)
  • Framework extension points
  • Polymorphic collections

```python

Good inheritance use

class Plugin(ABC): @abstractmethod def run(self): pass

class DataPlugin(Plugin): def run(self): return "Processing data"

class FilePlugin(Plugin): def run(self): return "Processing files"

def run_all(plugins: list[Plugin]): for plugin in plugins: plugin.run() ```

3. Refactoring Inheritance

Convert to composition:

```python

Before - Inheritance

class Logger: def log(self, msg): print(msg)

class TimestampLogger(Logger): def log(self, msg): from datetime import datetime super().log(f"{datetime.now()}: {msg}")

After - Composition

class Logger: def log(self, msg): print(msg)

class TimestampDecorator: def init(self, logger): self.logger = logger

def log(self, msg):
    from datetime import datetime
    self.logger.log(f"{datetime.now()}: {msg}")

logger = TimestampDecorator(Logger()) ```

Decision Checklist

Before choosing, run through these questions in order:

Quick Decision Checklist

  1. Is it a true type relationship? Can you say "B is-a A" and it sounds natural to a domain expert? → Inheritance
  2. Does it need to change at runtime? Will the behavior or component vary between instances or over the lifetime of an object? → Composition
  3. Is it built from parts? Is the object assembled from independent components? → Composition
  4. Is the hierarchy shallow and stable? Will there be 2–3 subclasses that are unlikely to change? → Inheritance is fine
  5. Still unsure?Default to composition. It is easier to refactor composition into inheritance than the reverse.

Summary Table

Criterion Inheritance Composition
Relationship is-a has-a
Coupling Tight Loose
Flexibility Low High
Reusability Limited High
Testing Harder Easier
Changes Cascading Isolated
Use for Type hierarchies Building systems

Exercises

Exercise 1. Refactor the following inheritance-based design into a composition-based design. FlyingFish inherits from both Fish and Bird. Instead, create SwimAbility and FlyAbility classes, and have FlyingFish compose them. Show that the composed version is easier to extend (e.g., adding RunAbility).

Solution to Exercise 1
# Composition-based design
class SwimAbility:
    def swim(self):
        return "Swimming through water"

class FlyAbility:
    def fly(self):
        return "Flying through air"

class RunAbility:
    def run(self):
        return "Running on land"

class FlyingFish:
    def __init__(self):
        self._swim = SwimAbility()
        self._fly = FlyAbility()

    def swim(self):
        return self._swim.swim()

    def fly(self):
        return self._fly.fly()

class SuperCreature:
    def __init__(self):
        self._swim = SwimAbility()
        self._fly = FlyAbility()
        self._run = RunAbility()

    def swim(self):
        return self._swim.swim()

    def fly(self):
        return self._fly.fly()

    def run(self):
        return self._run.run()

ff = FlyingFish()
print(ff.swim())  # Swimming through water
print(ff.fly())   # Flying through air

sc = SuperCreature()
print(sc.run())   # Running on land — easy to extend

Exercise 2. Compare an inheritance approach and a composition approach for modeling notifications. In the inheritance version, create Notification, EmailNotification, and SMSNotification classes. In the composition version, create a Notification class that accepts a Sender object (either EmailSender or SMSSender). Show how the composition version makes it easy to swap senders at runtime.

Solution to Exercise 2
# Composition version
class EmailSender:
    def send(self, recipient, message):
        return f"Email to {recipient}: {message}"

class SMSSender:
    def send(self, recipient, message):
        return f"SMS to {recipient}: {message}"

class Notification:
    def __init__(self, sender):
        self._sender = sender

    def notify(self, recipient, message):
        return self._sender.send(recipient, message)

    def set_sender(self, sender):
        self._sender = sender  # Swap at runtime!

notif = Notification(EmailSender())
print(notif.notify("alice@example.com", "Hello"))
# Email to alice@example.com: Hello

notif.set_sender(SMSSender())
print(notif.notify("555-1234", "Hello"))
# SMS to 555-1234: Hello

Exercise 3. Design a Character class for a game using composition instead of inheritance. Instead of subclasses like Warrior and Mage, create AttackStrategy and DefenseStrategy objects that can be swapped. A Character has a name, an attack strategy, and a defense strategy. Demonstrate creating different character types by composing different strategies.

Solution to Exercise 3
class SwordAttack:
    def attack(self):
        return "Swings sword for 20 damage"

class FireballAttack:
    def attack(self):
        return "Casts fireball for 35 damage"

class ShieldDefense:
    def defend(self):
        return "Blocks with shield, reducing damage by 15"

class MagicBarrier:
    def defend(self):
        return "Summons magic barrier, reducing damage by 25"

class Character:
    def __init__(self, name, attack_strategy, defense_strategy):
        self.name = name
        self._attack = attack_strategy
        self._defense = defense_strategy

    def attack(self):
        return f"{self.name}: {self._attack.attack()}"

    def defend(self):
        return f"{self.name}: {self._defense.defend()}"

warrior = Character("Warrior", SwordAttack(), ShieldDefense())
mage = Character("Mage", FireballAttack(), MagicBarrier())

print(warrior.attack())  # Warrior: Swings sword for 20 damage
print(warrior.defend())  # Warrior: Blocks with shield...
print(mage.attack())     # Mage: Casts fireball for 35 damage
print(mage.defend())     # Mage: Summons magic barrier...

Exercise 4. A junior developer writes the following inheritance hierarchy. Identify the design problem and refactor it to use composition. Explain why the original breaks down when a new requirement arrives: "some employees need both RemoteWork and OfficeWork capabilities."

```python class Employee: def work(self): return "Working"

class RemoteEmployee(Employee): def work(self): return "Working remotely"

class OfficeEmployee(Employee): def work(self): return "Working in office"

New requirement: HybridEmployee?

class HybridEmployee(RemoteEmployee, OfficeEmployee): pass # Diamond problem! ```

Solution to Exercise 4

The inheritance approach creates a diamond problem: HybridEmployee inherits from both RemoteEmployee and OfficeEmployee, which both override work(). Python's MRO resolves this deterministically, but the result (RemoteEmployee.work) is arbitrary — it does not represent "hybrid" behavior.

Refactored with composition:

class RemoteWork:
    def perform(self):
        return "Working remotely"

class OfficeWork:
    def perform(self):
        return "Working in office"

class Employee:
    def __init__(self, name, work_modes):
        self.name = name
        self._modes = work_modes

    def work(self):
        return [mode.perform() for mode in self._modes]

hybrid = Employee("Alice", [RemoteWork(), OfficeWork()])
print(hybrid.work())
# ['Working remotely', 'Working in office']

Composition avoids the diamond entirely. Adding a new work mode (e.g., CoworkingSpace) requires only a new class — no changes to Employee or existing modes.


Exercise 5. The "Interface Inheritance + Composition" pattern (shown earlier in this page) combines the best of both approaches. Explain why this pattern avoids the fragile base class problem. Then demonstrate it: define an ABC Exporter with an abstract export(data) method, create a JSONEngine and CSVEngine class, and implement JSONExporter and CSVExporter that inherit from Exporter but delegate to their respective engines via composition.

Solution to Exercise 5
from abc import ABC, abstractmethod
import json

class Exporter(ABC):
    @abstractmethod
    def export(self, data) -> str:
        pass

class JSONEngine:
    def convert(self, data) -> str:
        return json.dumps(data, indent=2)

class CSVEngine:
    def convert(self, data) -> str:
        if not data:
            return ""
        header = ",".join(data[0].keys())
        rows = [",".join(str(v) for v in row.values()) for row in data]
        return header + "\n" + "\n".join(rows)

class JSONExporter(Exporter):
    def __init__(self):
        self._engine = JSONEngine()  # Composition

    def export(self, data) -> str:
        return self._engine.convert(data)

class CSVExporter(Exporter):
    def __init__(self):
        self._engine = CSVEngine()  # Composition

    def export(self, data) -> str:
        return self._engine.convert(data)

data = [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]

for exporter in [JSONExporter(), CSVExporter()]:
    print(type(exporter).__name__)
    print(exporter.export(data))
    print()

Why this avoids the fragile base class problem: Exporter is abstract — it has no implementation to break. The actual conversion logic lives in the engine classes, which are independent of the inheritance hierarchy. Changing JSONEngine.convert() cannot affect CSVExporter, and adding a new engine requires no changes to Exporter. The inheritance provides a shared interface (polymorphism), while composition provides the implementation (flexibility).