Beyond the Hierarchy: Mastering Composition Over Inheritance in Python
Overview
Software design often forces a choice between two fundamental relationships: is-a and has-a. While inheritance allows a class to derive behavior from a parent, composition involves building complex objects by combining simpler, independent pieces. This guide demonstrates how to refactor rigid inheritance hierarchies into flexible, composed systems. By favoring composition, you reduce the strong coupling that often makes deep inheritance trees brittle and difficult to maintain. We will transform a cluttered employee payment system into a modular architecture where responsibilities like personnel data, payment logic, and commission structures remain distinct and interchangeable.
Prerequisites
To follow this tutorial, you should have a solid grasp of
Key Libraries & Tools
- abc: Python's built-in module for defining Abstract Base Classes, used to enforce method implementation in subclasses.
- dataclasses: A utility for automatically generating boilerplate code like
__init__for data-heavy classes. - typing: Specifically the
Optionaltype, used to handle components that may not exist for every instance.
Code Walkthrough

The Problem: Inheritance Explosion
Initially, developers often reach for inheritance. If you have an Employee and need to add a commission, you might create a SalariedEmployeeWithCommission. When you add a bonus, you create a SalariedEmployeeWithCommissionAndBonus. This leads to a combinatorial explosion of subclasses.
The Solution: Modular Composition
First, we define our interfaces using
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional
class Contract(ABC):
@abstractmethod
def get_payment(self) -> float:
pass
class Commission(ABC):
@abstractmethod
def get_payment(self) -> float:
pass
Next, we implement concrete versions of these components. This isolates the logic for hourly pay from the logic for sales commissions.
@dataclass
class HourlyContract(Contract):
pay_rate: float
hours_worked: float
employer_cost: float = 1000
def get_payment(self) -> float:
return self.pay_rate * self.hours_worked + self.employer_cost
@dataclass
class ContractCommission(Commission):
commission_amount: float
contracts_landed: int
def get_payment(self) -> float:
return self.commission_amount * self.contracts_landed
Finally, the Employee class becomes a container. It doesn't care how pay is calculated; it simply delegates that task to its composed components.
@dataclass
class Employee:
name: string
id: int
contract: Contract
commission: Optional[Commission] = None
def compute_pay(self) -> float:
payout = self.contract.get_payment()
if self.commission:
payout += self.commission.get_payment()
return payout
Syntax Notes
We utilize the @dataclass decorator to keep the code clean and readable, removing the need for manual __init__ methods. The Optional[Commission] syntax indicates that while every employee must have a contract, the commission component is a flexible add-on. This pattern adheres to the Employee depends on the Contract abstraction rather than a concrete implementation.
Practical Examples
This pattern is highly effective in SalaryContract for an HourlyContract at runtime, you can change an object's behavior without modifying its class. It is also prevalent in game development (ECS patterns) where entities gain abilities by adding components rather than inheriting from a "Super Warrior" class.
Tips & Gotchas
Avoid deep inheritance chains; most design patterns from the super() to constantly patch behavior, you likely need composition. Use