Skip to content

Composition vs Inheritance

Core Differences

1. Relationship Type

Inheritance models "is-a" relationships:

class Animal:
    def speak(self):
        pass

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

Composition models "has-a" relationships:

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:

class Vehicle:
    def move(self):
        return "Moving"

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

Composition reuses through delegation:

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:

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:

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:

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:

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:

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:

# ❌ 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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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

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

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"

# 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

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

# 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:

# 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())

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