Loading episodes…
0:00 0:00

Visually Explained: Master the Strategy Pattern in Python

00:00
BACK TO HOME

Visually Explained: Master the Strategy Pattern in Python

10xTeam December 23, 2025 12 min read

Have you ever found yourself trapped in a labyrinth of if-elif-else statements? You start with a simple function, but as new requirements roll in, it grows into a monolithic beast that’s terrifying to modify and impossible to test. This common scenario is a direct violation of core software design principles, leading to brittle, unmaintainable code.

Enter the Strategy Pattern, a behavioral design pattern that offers an elegant escape. It allows you to define a family of algorithms, encapsulate each one in a separate class, and make them interchangeable. Instead of a rigid structure, you get a flexible system where you can select the right algorithm at runtime.

Think of it like a professional golfer. Depending on the terrain, distance, and wind, they choose a specific club (a driver, an iron, a putter) to make the shot. The goal is the same—get the ball in the hole—but the strategy (the club) changes with the context. The Strategy Pattern brings this same flexibility to your code.

The Problem: The Unmaintainable Salary Calculator

Let’s ground this in a practical example. Imagine we’re building a payroll system. The company has different types of employees—full-time, hourly, freelancers—and each has a unique salary calculation formula.

A naive approach might look like this:

from dataclasses import dataclass
from enum import Enum, auto

class EmployeeType(Enum):
    FULL_TIME = auto()
    HOURLY = auto()
    FREELANCE = auto()
    INTERN = auto()

@dataclass
class Employee:
    name: str
    type: EmployeeType
    base_salary: int = 0
    bonus: int = 0
    hours_worked: int = 0
    hourly_rate: int = 0
    gross_amount: int = 0

def calculate_salary(employee: Employee) -> float:
    """Calculates salary based on employee type using conditional logic."""
    if employee.type == EmployeeType.FULL_TIME:
        return employee.base_salary + employee.bonus
    elif employee.type == EmployeeType.HOURLY:
        return employee.hours_worked * employee.hourly_rate
    elif employee.type == EmployeeType.FREELANCE:
        # Freelancers have taxes and commissions deducted
        tax = employee.gross_amount * 0.10
        commission = employee.gross_amount * 0.05
        return employee.gross_amount - tax - commission
    elif employee.type == EmployeeType.INTERN:
        return 1000.0  # Fixed stipend
    else:
        raise ValueError(f"Unknown employee type: {employee.type}")

# --- Usage ---
full_time_emp = Employee(name="Ahmed", type=EmployeeType.FULL_TIME, base_salary=50000, bonus=10000)
freelance_emp = Employee(name="Maria", type=EmployeeType.FREELANCE, gross_amount=70000)

print(f"{full_time_emp.name}'s Salary: ${calculate_salary(full_time_emp)}")
print(f"{freelance_emp.name}'s Salary: ${calculate_salary(freelance_emp)}")

[!WARNING] This calculate_salary function is a ticking time bomb.

  1. Violates Single Responsibility Principle (SRP): The function is responsible for all calculation algorithms.
  2. Violates Open/Closed Principle (OCP): To add a new employee type (e.g., “Contractor”), you must modify this function, risking the introduction of bugs into existing logic.
  3. Hard to Test: You need to write complex tests to cover every single branch of this function.

The Solution: Refactoring with the Strategy Pattern

Let’s dismantle this monolith and rebuild it using the Strategy Pattern. We’ll organize our code into a clean, maintainable structure.

First, let’s visualize the new structure we’re aiming for.

payroll_system/
├── main.py
├── employee.py
└── strategies/
    ├── __init__.py
    ├── interface.py
    ├── full_time.py
    ├── hourly.py
    ├── freelance.py
    └── intern.py

Step 1: Define the Strategy Interface

The “interface” is a contract that all our concrete strategies must follow. In Python, we define this using an Abstract Base Class (ABC). It ensures every strategy has the same method signature.

# strategies/interface.py
from abc import ABC, abstractmethod
from employee import Employee # Assuming employee.py contains the Employee class

class SalaryStrategy(ABC):
    """The Strategy Interface for calculating salary."""

    @abstractmethod
    def calculate(self, employee: Employee) -> float:
        """Calculate the salary for a given employee."""
        pass

Step 2: Implement Concrete Strategies

Now, we encapsulate each if block’s logic into its own class. Each class inherits from our SalaryStrategy and provides a concrete implementation for the calculate method.

Full-Time Strategy:

# strategies/full_time.py
from .interface import SalaryStrategy
from employee import Employee

class FullTimeStrategy(SalaryStrategy):
    def calculate(self, employee: Employee) -> float:
        return employee.base_salary + employee.bonus

Hourly Strategy:

# strategies/hourly.py
from .interface import SalaryStrategy
from employee import Employee

class HourlyStrategy(SalaryStrategy):
    def calculate(self, employee: Employee) -> float:
        return employee.hours_worked * employee.hourly_rate

Freelance Strategy:

# strategies/freelance.py
from .interface import SalaryStrategy
from employee import Employee

class FreelanceStrategy(SalaryStrategy):
    def calculate(self, employee: Employee) -> float:
        tax = employee.gross_amount * 0.10
        commission = employee.gross_amount * 0.05
        return employee.gross_amount - tax - commission

Intern Strategy:

# strategies/intern.py
from .interface import SalaryStrategy
from employee import Employee

class InternStrategy(SalaryStrategy):
    def calculate(self, employee: Employee) -> float:
        return 1000.0

Step 3: Define the Context

The “Context” is the object that uses a strategy. It doesn’t know the details of any specific algorithm; it just knows how to use the strategy object it’s given. This decouples the context from the concrete implementations.

# main.py (or a new payroll_context.py)
from employee import Employee
from strategies.interface import SalaryStrategy

class PayrollContext:
    """The Context that uses a salary calculation strategy."""

    def __init__(self, strategy: SalaryStrategy):
        self._strategy = strategy

    def set_strategy(self, strategy: SalaryStrategy):
        """Allows changing the strategy at runtime."""
        self._strategy = strategy

    def calculate_salary(self, employee: Employee) -> float:
        """Delegates the calculation to the current strategy object."""
        return self._strategy.calculate(employee)

This class diagram illustrates the relationships we’ve just built:

classDiagram
    class PayrollContext {
        -SalaryStrategy strategy
        +set_strategy(strategy)
        +calculate_salary(employee)
    }
    class SalaryStrategy {
        <<interface>>
        +calculate(employee)*
    }
    class FullTimeStrategy {
        +calculate(employee)
    }
    class HourlyStrategy {
        +calculate(employee)
    }
    class FreelanceStrategy {
        +calculate(employee)
    }

    PayrollContext o-- SalaryStrategy
    SalaryStrategy <|-- FullTimeStrategy
    SalaryStrategy <|-- HourlyStrategy
    SalaryStrategy <|-- FreelanceStrategy

Step 4: Putting It All Together

Now, our client code is much cleaner. Instead of a giant if block, we simply choose the right strategy and pass it to our context.

Here’s how the logic evolves, shown as a diff:

- def calculate_salary(employee: Employee) -> float:
-     """Calculates salary based on employee type using conditional logic."""
-     if employee.type == EmployeeType.FULL_TIME:
-         return employee.base_salary + employee.bonus
-     elif employee.type == EmployeeType.HOURLY:
-         return employee.hours_worked * employee.hourly_rate
-     elif employee.type == EmployeeType.FREELANCE:
-         tax = employee.gross_amount * 0.10
-         commission = employee.gross_amount * 0.05
-         return employee.gross_amount - tax - commission
-     elif employee.type == EmployeeType.INTERN:
-         return 1000.0
-     else:
-         raise ValueError(f"Unknown employee type: {employee.type}")

+ # --- New, clean approach ---
+ from strategies import FullTimeStrategy, FreelanceStrategy
+ 
+ full_time_emp = Employee(name="Ahmed", type=EmployeeType.FULL_TIME, base_salary=50000, bonus=10000)
+ freelance_emp = Employee(name="Maria", type=EmployeeType.FREELANCE, gross_amount=70000)
+ 
+ # Calculate for Ahmed
+ context = PayrollContext(FullTimeStrategy())
+ ahmed_salary = context.calculate_salary(full_time_emp)
+ print(f"{full_time_emp.name}'s Salary: ${ahmed_salary}")
+ 
+ # Calculate for Maria by changing the strategy
+ context.set_strategy(FreelanceStrategy())
+ maria_salary = context.calculate_salary(freelance_emp)
+ print(f"{freelance_emp.name}'s Salary: ${maria_salary}")

Dynamic Strategy Selection with a Factory

Manually creating strategy instances in the client code is good, but we can do even better. We can hide the creation logic behind a Factory. A factory is a function or class that’s responsible for creating objects for us. This further decouples the client, which no longer needs to know about the concrete strategy classes at all.

A dictionary is a very Pythonic way to implement a simple factory.

# strategies/__init__.py

from .interface import SalaryStrategy
from .full_time import FullTimeStrategy
from .hourly import HourlyStrategy
from .freelance import FreelanceStrategy
from .intern import InternStrategy
from employee import EmployeeType

# The Factory: A simple dictionary mapping types to strategy classes
STRATEGY_MAP = {
    EmployeeType.FULL_TIME: FullTimeStrategy,
    EmployeeType.HOURLY: HourlyStrategy,
    EmployeeType.FREELANCE: FreelanceStrategy,
    EmployeeType.INTERN: InternStrategy,
}

def get_strategy(employee_type: EmployeeType) -> SalaryStrategy:
    """Factory function to get the correct strategy instance."""
    strategy_class = STRATEGY_MAP.get(employee_type)
    if not strategy_class:
        raise ValueError(f"No strategy found for type: {employee_type}")
    return strategy_class()

Now, the client code becomes incredibly simple and robust:

# main.py
from employee import Employee, EmployeeType
from strategies import get_strategy
from payroll_context import PayrollContext # Assuming PayrollContext is in its own file

# --- Usage ---
employee = Employee(name="Mohammed", type=EmployeeType.HOURLY, hours_worked=100, hourly_rate=30)

# 1. Get the strategy from the factory
strategy = get_strategy(employee.type)

# 2. Use the context to calculate
context = PayrollContext(strategy)
salary = context.calculate_salary(employee)

print(f"{employee.name}'s Salary: ${salary}")

The flow is now beautifully decoupled:

graph TD
    A[Client Code] --> B{get_strategy(employee.type)};
    B --> C{Strategy Factory};
    C -- Selects based on type --> D[Instantiate Correct Strategy];
    D --> A;
    A --> E[PayrollContext];
    E -- Executes --> F[Strategy.calculate()];
    F --> E;
    E --> A;

[!TIP] Stateless vs. Stateful Strategies Our salary strategies are stateless—they don’t store any data that changes between calls. This is ideal, as we can reuse a single instance. If a strategy needed to track state, you would want the factory to create a new instance for each request to avoid data from one calculation leaking into another.

Conclusion: Why the Strategy Pattern Wins

By refactoring our code, we’ve achieved several key benefits:

  • Maintainability: Each algorithm lives in its own file. Changes are isolated and safe.
  • Scalability: Adding a new Contractor employee type is as simple as creating a ContractorStrategy class and adding it to the factory’s map. No existing code needs to be touched.
  • Testability: Each strategy can be unit-tested in complete isolation.
  • Readability: The logic is no longer a tangled mess but a clean, high-level orchestration.

The Strategy Pattern is a powerful tool for turning rigid, conditional logic into a flexible and robust system that embraces change.

To summarize the key ideas:

mindmap
  root((Strategy Pattern))
    What?
      :A behavioral design pattern.
      :Lets you define a family of algorithms.
      :Puts each algorithm in a separate class.
      :Makes their objects interchangeable.
    Why?
      :Avoids complex if-else chains.
      :Follows Open/Closed Principle.
      :Improves code maintainability.
    Components
      Context
        :The class that uses a strategy.
        :Holds a reference to a strategy object.
        :Delegates work to the strategy.
      Strategy (Interface)
        :A common interface for all algorithms.
        :Ensures interchangeability.
      Concrete Strategy
        :Implements a specific algorithm.


Quiz: Test Your Knowledge! 1. **What is the primary SOLID principle that the Strategy Pattern helps enforce?**
Solution The **Open/Closed Principle (OCP)**. The system is *open* for extension (by adding new strategy classes) but *closed* for modification (you don't need to change the context or existing strategies). It also strongly supports the **Single Responsibility Principle (SRP)**.

2. **In our Python example, what is the role of the `SalaryStrategy` ABC?**
Solution It acts as the **Strategy Interface**. It defines a common contract (`calculate` method) that all concrete strategy classes must adhere to, ensuring they are interchangeable from the perspective of the `PayrollContext`.

3. **Why is using a Factory to select the strategy often a good idea?**
Solution A Factory **decouples the client code from the concrete strategy implementations**. The client no longer needs to know which specific strategy classes exist; it only needs to request a strategy for a given type (e.g., `EmployeeType.FREELANCE`), making the client code simpler and more maintainable.

Join the 10xdev Community

Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.

Audio Interrupted

We lost the audio stream. Retry with shorter sentences?