Mastering the Strategy Design Pattern: From Classic OOP to Functional Python
Overview of the Strategy Pattern
The Strategy design pattern is a behavioral pattern that allows you to define a family of algorithms, encapsulate each one, and make them interchangeable. This pattern matters because it lets the algorithm vary independently from the clients that use it. In a typical application, you might find yourself drowning in if-else or switch statements to handle different logic branches. The Strategy pattern cleans up this mess by injecting behavior into an application without the core logic needing to know the implementation details. It effectively reduces coupling and adheres to the Open/Closed Principle: your code is open for extension but closed for modification.
Prerequisites

To get the most out of this tutorial, you should have a solid grasp of:
- Python 3.8+: Familiarity with modern syntax.
- Object-Oriented Programming (OOP): Understanding classes, inheritance, and methods.
- Type Hinting: Basic knowledge of the
typingmodule. - Functional Basics: Comfort with the idea that functions can be passed around as arguments.
Key Libraries & Tools
- ABC (Abstract Base Classes): Used to define formal interfaces in the classic OOP approach.
- Typing (Protocol & Callable): Modern tools for structural subtyping and defining function signatures.
- Dataclasses: A concise way to create classes that primarily store data.
- Tabnine: An AI-powered code completion assistant used during the implementation to speed up development.
Code Walkthrough: Evolution of Implementation
1. The Classic OOP Approach
The traditional way involves an
from abc import ABC, abstractmethod
class TicketOrderingStrategy(ABC):
@abstractmethod
def create_ordering(self, tickets: list) -> list:
pass
class FIFOOrderingStrategy(TicketOrderingStrategy):
def create_ordering(self, tickets: list) -> list:
return tickets.copy()
Here, TicketOrderingStrategy acts as the interface. The CustomerSupport class would receive an instance of a subclass and call create_ordering. While robust, it requires significant boilerplate code.
2. Modern Structural Subtyping with Protocols
from typing import Protocol
class TicketOrderingStrategy(Protocol):
def create_ordering(self, tickets: list) -> list:
...
This removes the rigid inheritance chain and reduces dependencies. As long as your class has a create_ordering method, it satisfies the requirement.
3. The Functional Callable Approach
In Python, functions are first-class citizens. We can simplify further by using the __call__ dunder method or raw functions. By defining the strategy as a Callable, we treat classes and functions as interchangeable entities.
from typing import Callable
# Type alias for our strategy
TicketOrderingStrategy = Callable[[list], list]
def fifo_strategy(tickets: list) -> list:
return tickets.copy()
This is the leanest implementation. The CustomerSupport class simply calls the strategy as if it were a function: ordered_list = self.strategy(self.tickets).
Syntax Notes
- The
__call__Dunder: Implementing this method makes an object "callable" like a function. It is perfect for strategies that need to maintain state while behaving like an algorithm. - Type Aliases: Using
TicketOrderingStrategy = Callable[...]makes your type hints readable without the overhead of a full class definition. - Duck Typing: Protocols facilitate duck typing by checking if an object "walks and quacks" like the required interface at type-check time.
Practical Examples
- Support Ticket Systems: Sorting tickets by priority, age (FIFO/LIFO), or random assignment for load balancing.
- E-commerce Checkout: Implementing various discount strategies (seasonal, bulk, or loyalty) that can be swapped based on the user profile.
- Data Export: A system that can export data to CSV, JSON, or XML depending on the user's selection.
Tips & Gotchas
- Parameter Passing: Functions are great until you need to pass extra configuration (like a random seed). For these cases, use Closures or Classes. A closure allows you to "bake in" parameters into a returned function.
- Over-Engineering: Don't use a full ABC if a simple function will do. Start with the simplest functional approach and only move to classes if you need to manage complex internal state.
- Copying Data: Always return a copy of the list (e.g.,
tickets.copy()) within your strategy to avoid unintended side effects on the original data source.

Fancy watching it?
Watch the full video and context