Skip to content

Composition vs Inheritance

Choose the right relationship: 'is-a' vs 'has-a'.

Composition and inheritance are two fundamental ways to build relationships between classes. Understanding when to use each is crucial for writing maintainable, flexible code.

  • Inheritance creates an “is-a” relationship (Car is-a Vehicle)
  • Composition creates a “has-a” relationship (Car has-a Engine)

Inheritance is used when a class is a specialized version of another class.

inheritance_example.py
class Vehicle:
def __init__(self, brand: str, model: str):
self.brand = brand
self.model = model
def start(self):
return f"{self.brand} {self.model} started."
class Car(Vehicle):
"""Car IS-A Vehicle"""
def __init__(self, brand: str, model: str, num_doors: int):
super().__init__(brand, model)
self.num_doors = num_doors
car = Car("Toyota", "Camry", 4)
print(car.start()) # Inherited method

Composition is used when a class contains or uses another class.

composition_example.py
class Engine:
def start(self):
return "Engine started."
def stop(self):
return "Engine stopped."
class Car:
"""Car HAS-A Engine"""
def __init__(self, brand: str, model: str):
self.brand = brand
self.model = model
self.engine = Engine() # Composition
def start(self):
return f"{self.brand} {self.model}: {self.engine.start()}"
car = Car("Toyota", "Camry")
print(car.start()) # "Toyota Camry: Engine started."

Use inheritance when:

  • The relationship is truly “is-a”
  • You need to override methods
  • You want to share common behavior
  • The subclass is a specialized version of the parent
good_inheritance.py
class Animal:
def make_sound(self):
raise NotImplementedError
class Dog(Animal):
"""Dog IS-A Animal - good use of inheritance"""
def make_sound(self):
return "Woof!"
class Cat(Animal):
"""Cat IS-A Animal - good use of inheritance"""
def make_sound(self):
return "Meow!"

Use composition when:

  • The relationship is “has-a” or “uses-a”
  • You need flexibility to change behavior at runtime
  • You want to avoid deep inheritance hierarchies
  • The relationship is more about functionality than identity
good_composition.py
class Engine:
def start(self):
return "Engine started"
class ElectricMotor:
def start(self):
return "Motor started"
class Car:
"""Car HAS-A engine/motor - good use of composition"""
def __init__(self, brand: str, model: str, power_source):
self.brand = brand
self.model = model
self.power_source = power_source # Can be Engine or ElectricMotor
def start(self):
return f"{self.brand} {self.model}: {self.power_source.start()}"
# Flexible - can use different power sources
gas_car = Car("Toyota", "Camry", Engine())
electric_car = Car("Tesla", "Model 3", ElectricMotor())
print(gas_car.start()) # "Toyota Camry: Engine started"
print(electric_car.start()) # "Tesla Model 3: Motor started"
inheritance_employee.py
class Employee:
def __init__(self, name: str, employee_id: int):
self.name = name
self.employee_id = employee_id
def calculate_salary(self):
return 0
class FullTimeEmployee(Employee):
def __init__(self, name: str, employee_id: int, salary: float):
super().__init__(name, employee_id)
self.salary = salary
def calculate_salary(self):
return self.salary
class PartTimeEmployee(Employee):
def __init__(self, name: str, employee_id: int, hourly_rate: float, hours: float):
super().__init__(name, employee_id)
self.hourly_rate = hourly_rate
self.hours = hours
def calculate_salary(self):
return self.hourly_rate * self.hours
# Problem: What if an employee changes from full-time to part-time?
# You'd need to create a new object - not flexible!
composition_employee.py
class SalaryCalculator:
"""Strategy pattern using composition"""
def calculate(self, employee_data: dict) -> float:
raise NotImplementedError
class FullTimeSalaryCalculator(SalaryCalculator):
def calculate(self, employee_data: dict) -> float:
return employee_data["salary"]
class PartTimeSalaryCalculator(SalaryCalculator):
def calculate(self, employee_data: dict) -> float:
return employee_data["hourly_rate"] * employee_data["hours"]
class ContractSalaryCalculator(SalaryCalculator):
def calculate(self, employee_data: dict) -> float:
return employee_data["contract_amount"]
class Employee:
"""Employee HAS-A salary calculator - flexible!"""
def __init__(self, name: str, employee_id: int, calculator: SalaryCalculator):
self.name = name
self.employee_id = employee_id
self.calculator = calculator # Composition
self.employee_data = {}
def set_employee_data(self, **kwargs):
self.employee_data = kwargs
def calculate_salary(self):
return self.calculator.calculate(self.employee_data)
def change_calculator(self, new_calculator: SalaryCalculator):
"""Can change salary calculation at runtime!"""
self.calculator = new_calculator
# Usage - much more flexible
full_time_calc = FullTimeSalaryCalculator()
employee = Employee("Alice", 101, full_time_calc)
employee.set_employee_data(salary=50000)
print(employee.calculate_salary()) # 50000
# Change to part-time without creating new object
part_time_calc = PartTimeSalaryCalculator()
employee.change_calculator(part_time_calc)
employee.set_employee_data(hourly_rate=25, hours=20)
print(employee.calculate_salary()) # 500
Diagram
inheritance_notifications.py
class Notification:
def send(self, message: str):
raise NotImplementedError
class EmailNotification(Notification):
def send(self, message: str):
print(f"Sending email: {message}")
class SMSNotification(Notification):
def send(self, message: str):
print(f"Sending SMS: {message}")
# Problem: What if you need multiple notification channels?
# You'd need to create multiple classes or use multiple inheritance
composition_notifications.py
class EmailService:
def send(self, recipient: str, message: str):
print(f"Email to {recipient}: {message}")
class SMSService:
def send(self, phone: str, message: str):
print(f"SMS to {phone}: {message[:50]}...")
class PushService:
def send(self, device_id: str, message: str):
print(f"Push to {device_id}: {message}")
class Notification:
"""Notification HAS-A list of services - flexible!"""
def __init__(self):
self.services = [] # Composition - can have multiple
def add_service(self, service):
self.services.append(service)
def send(self, message: str, **kwargs):
"""Send through all services"""
for service in self.services:
if isinstance(service, EmailService):
service.send(kwargs.get("email"), message)
elif isinstance(service, SMSService):
service.send(kwargs.get("phone"), message)
elif isinstance(service, PushService):
service.send(kwargs.get("device_id"), message)
# Usage - very flexible
notification = Notification()
notification.add_service(EmailService())
notification.add_service(SMSService())
notification.add_service(PushService())
notification.send(
"Your order has shipped!",
email="user@example.com",
phone="+1234567890",
device_id="device-123"
)

The “Favor Composition Over Inheritance” Principle

Section titled “The “Favor Composition Over Inheritance” Principle”

This principle suggests that composition is often more flexible than inheritance:

deep_inheritance.py
class Animal:
pass
class Mammal(Animal):
pass
class Dog(Mammal):
pass
class Labrador(Dog):
pass
class GoldenRetriever(Dog):
pass
# Problem: What if a Labrador needs behavior from GoldenRetriever?
# You can't easily share code without creating more complex hierarchies
composition_solution.py
class Behavior:
"""Composable behaviors"""
def execute(self):
raise NotImplementedError
class FetchBehavior(Behavior):
def execute(self):
return "Fetching ball..."
class GuardBehavior(Behavior):
def execute(self):
return "Guarding territory..."
class PlayfulBehavior(Behavior):
def execute(self):
return "Playing fetch..."
class Dog:
"""Dog HAS-A behavior - can change at runtime!"""
def __init__(self, name: str, behavior: Behavior):
self.name = name
self.behavior = behavior # Composition
def perform_behavior(self):
return self.behavior.execute()
def change_behavior(self, new_behavior: Behavior):
"""Change behavior dynamically"""
self.behavior = new_behavior
# Flexible - can change behaviors
dog = Dog("Buddy", FetchBehavior())
print(dog.perform_behavior()) # "Fetching ball..."
dog.change_behavior(GuardBehavior())
print(dog.perform_behavior()) # "Guarding territory..."
  • Deep inheritance hierarchies can become hard to maintain
  • Composition allows you to change behavior at runtime
  • Both have their place - choose based on the specific use case

Remember: “Favor composition over inheritance” is a guideline, not a rule. Use inheritance when it makes sense, but prefer composition when you need flexibility.