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 Python basics, including classes and methods. Familiarity with Object-Oriented Programming concepts like classes and subclasses is necessary. Understanding the basics of Abstract Base Classes will help in understanding how we define interfaces.
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 Abstract Base Classes. Instead of one giant hierarchy, we create separate modules for different responsibilities.
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 Dependency Inversion principle, as the Employee depends on the Contract abstraction rather than a concrete implementation.
Practical Examples
This pattern is highly effective in FinTech applications where payment rules change frequently. By swapping a 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 Gang of Four Design Patterns rarely exceed one or two levels. Inheritance creates the strongest possible coupling in OOP, meaning a change in the parent can silently break every child. If you find yourself using super() to constantly patch behavior, you likely need composition. Use Abstract Base Classes to define clear boundaries between your system's modules.
- Abstract Base Classes
- 33%· softwaredesign
- Dependency Injection
- 11%· softwaredesign
- Dependency Inversion
- 11%· softwaredesign
- Gang of Four Design Patterns
- 11%· books
- Object-Oriented Programming
- 11%· softwaredesign
- Other topics
- 22%

Composition Is Better Than Inheritance in Python
WatchArjanCodes // 23:29
On this channel, I post videos about programming and software design to help you take your coding skills to the next level. I'm an entrepreneur and a university lecturer in computer science, with more than 20 years of experience in software development and design. If you're a software developer and you want to improve your development skills, and learn more about programming in general, make sure to subscribe for helpful videos. I post a video here every Friday. If you have any suggestion for a topic you'd like me to cover, just leave a comment on any of my videos and I'll take it under consideration. Thanks for watching!