Beyond the Hierarchy: Mastering Composition Over Inheritance in Python

ArjanCodes////4 min read

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 Optional type, used to handle components that may not exist for every instance.

Code Walkthrough

Beyond the Hierarchy: Mastering Composition Over Inheritance in Python
Composition Is Better Than Inheritance in Python

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.

Topic DensityMention share of the most discussed topics · 9 mentions across 7 distinct topics
Abstract Base Classes
33%· softwaredesign
Dependency Injection
11%· softwaredesign
Dependency Inversion
11%· softwaredesign
Object-Oriented Programming
11%· softwaredesign
Other topics
22%
End of Article
Source video
Beyond the Hierarchy: Mastering Composition Over Inheritance in Python

Composition Is Better Than Inheritance in Python

Watch

ArjanCodes // 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!

What they talk about
AI and Agentic Coding News
Who and what they mention most
Python
27.3%3
Python
18.2%2
Python
18.2%2
4 min read0%
4 min read