Implementing the Strategy Pattern in Python
The Strategy Pattern is one of the most useful behavioral design patterns in software engineering. It allows you to define a family of algorithms, encapsulate each one, and make them interchangeable at runtime. Let us delve into understanding how to implement the strategy pattern in python.
1. What is Strategy Pattern? When to Use It?
The Strategy Pattern is a behavioral design pattern that allows you to define a family of algorithms, encapsulate each one into separate classes, and make them interchangeable at runtime. Instead of embedding multiple conditional branches (like if-else or switch) inside a single class, the Strategy Pattern delegates the behavior to different strategy objects.
This pattern follows the principle of “composition over inheritance”, where behavior is composed dynamically rather than inherited statically. The client (or context) can switch between different strategies without modifying its own code, making the system more flexible and easier to extend.
In simple terms, the Strategy Pattern lets you change how something works while the program is running, without rewriting or modifying existing logic.
1.1 Key Characteristics
- Encapsulates different behaviors (algorithms): Each algorithm is placed in its own class, making it independent and reusable.
- Promotes composition over inheritance: Instead of relying on subclassing to change behavior, you inject the desired strategy into the context.
- Supports runtime flexibility: You can dynamically switch strategies based on user input, configuration, or application state.
- Eliminates large conditional blocks: Replaces complex
if-elseorswitchstatements with clean, maintainable code. - Follows Open/Closed Principle: New strategies can be added without modifying existing code, reducing the risk of introducing bugs.
- Improves testability: Each strategy can be tested independently, making unit testing simpler and more focused.
1.2 When to Use Strategy Pattern
- When you have multiple ways to perform a task: For example, different sorting algorithms, payment methods, or pricing strategies.
- When you need to switch behavior dynamically at runtime: Such as selecting different compression algorithms or authentication mechanisms.
- When your code has many conditional statements: If you find yourself writing long chains of
if-elseblocks based on behavior, Strategy Pattern can simplify the design. - When you want to isolate algorithm logic: Separating business logic from algorithm implementation improves readability and maintainability.
- When you expect frequent changes in behavior: If new algorithms or variations are likely to be added in the future, this pattern makes extensions easier.
- When different variants of an algorithm share the same interface: Strategy Pattern ensures consistency while allowing different implementations.
1.3 Real-World Examples
- Payment processing: Credit card, PayPal, UPI — each method is a different strategy.
- Sorting data: Choosing between quick sort, merge sort, or custom sorting logic.
- Discount calculation: Seasonal discounts, coupon-based discounts, or membership pricing.
- Compression or encryption: Switching between algorithms like ZIP, GZIP, or AES.
1.4 Benefits
- Cleaner and more maintainable code
- Easy to extend with new strategies
- Reduces code duplication
- Encourages modular design
1.5 Trade-offs
- Increases the number of classes in the system
- Clients must be aware of different strategies
- May introduce slight overhead due to abstraction
2. Code Example
2.1 Simple Strategy Pattern Example
Below is a simpler example without abstract base classes:
# Simple strategy functions
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def multiply(a, b):
return a * b
class Calculator:
def __init__(self, strategy):
self.strategy = strategy
def set_strategy(self, strategy):
self.strategy = strategy
def execute(self, a, b):
return self.strategy(a, b)
calc = Calculator(add)
print(calc.execute(10, 5))
calc.set_strategy(subtract)
print(calc.execute(10, 5))
calc.set_strategy(multiply)
print(calc.execute(10, 5))
2.1.1 Code Explanation
This example demonstrates a simplified version of the Strategy Pattern using functions instead of classes, where add, subtract, and multiply act as interchangeable strategies that define different behaviors for performing calculations; the Calculator class serves as the context and accepts a strategy (function) during initialization, storing it in the strategy attribute, and provides a set_strategy() method to dynamically change the behavior at runtime; the execute() method delegates the operation to the currently assigned strategy by calling it with the given inputs a and b; in the client code, the calculator is first initialized with the add function and executes addition, then switches to subtract and performs subtraction, and finally switches to multiply to perform multiplication, illustrating how functions can be used as lightweight strategies in Python to achieve flexible and dynamic behavior without the need for class-based implementations.
2.1.2 Output
15 5 50
The output reflects how the Calculator dynamically changes its behavior using different strategy functions. Initially, the calculator is initialized with the add function, so calling execute(10, 5) returns 15. Then, the strategy is updated to subtract using the set_strategy() method, and the same inputs now produce 5. Finally, the strategy is switched to multiply, resulting in 50. This demonstrates that the calculator’s behavior can be altered at runtime simply by assigning a different function, without changing the internal implementation of the Calculator class, showcasing a clean and flexible use of the Strategy Pattern in Python.
2.2 Using Abstract Base Classes
We will implement a payment system where different payment strategies (Credit Card, PayPal, UPI) can be swapped at runtime.
from abc import ABC, abstractmethod
# Strategy Interface
class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount):
pass
# Concrete Strategies
class CreditCardPayment(PaymentStrategy):
def pay(self, amount):
return f"Paid {amount} using Credit Card"
class PayPalPayment(PaymentStrategy):
def pay(self, amount):
return f"Paid {amount} using PayPal"
class UPIPayment(PaymentStrategy):
def pay(self, amount):
return f"Paid {amount} using UPI"
# Context Class
class ShoppingCart:
def __init__(self, strategy: PaymentStrategy):
self._strategy = strategy
def set_strategy(self, strategy: PaymentStrategy):
print("Switching payment strategy...")
self._strategy = strategy
def checkout(self, amount):
return self._strategy.pay(amount)
# Client Code
if __name__ == "__main__":
cart = ShoppingCart(CreditCardPayment())
print(cart.checkout(1000))
# Swap strategy at runtime
cart.set_strategy(PayPalPayment())
print(cart.checkout(2000))
cart.set_strategy(UPIPayment())
print(cart.checkout(3000))
2.2.1 Code Explanation
This example demonstrates the Strategy Pattern by defining a common interface PaymentStrategy using Python’s ABC module, where the abstract method pay() enforces a contract that all payment strategies must implement; three concrete strategies—CreditCardPayment, PayPalPayment, and UPIPayment—provide their own implementations of the pay() method, each representing a different payment algorithm; the ShoppingCart class acts as the context, holding a reference to a strategy object and delegating the payment operation to it via the checkout() method, while also allowing the strategy to be changed dynamically using set_strategy(); in the client code, a ShoppingCart is initialized with a CreditCardPayment strategy, and then the strategy is switched at runtime to PayPalPayment and UPIPayment without modifying the cart’s internal logic, illustrating how the Strategy Pattern enables flexible, interchangeable behaviors and eliminates the need for conditional statements.
2.2.2 Code Output
Paid 1000 using Credit Card Switching payment strategy... Paid 2000 using PayPal Switching payment strategy... Paid 3000 using UPI
The output shows how the ShoppingCart dynamically changes its payment behavior at runtime using different strategy objects. Initially, the cart is created with the CreditCardPayment strategy, so when checkout(1000) is called, it delegates the call to that strategy and prints “Paid 1000 using Credit Card”. Next, the set_strategy() method is used to switch to PayPalPayment, which prints a message indicating the strategy change and then processes the payment using PayPal, resulting in “Paid 2000 using PayPal”. Finally, the strategy is switched again to UPIPayment, and the cart processes the payment accordingly, producing “Paid 3000 using UPI”. This demonstrates that the behavior of the cart can be modified at runtime without changing its internal implementation, highlighting the flexibility and clean separation of concerns provided by the Strategy Pattern.
3. Conclusion
In conclusion, the Strategy Pattern is an effective design approach that enhances flexibility and maintainability by encapsulating different algorithms into separate classes or functions, allowing behaviors to be swapped seamlessly without altering existing code. It improves code readability, adheres to the open/closed principle by enabling extensions without modifications, reduces the complexity of conditional statements, and provides runtime flexibility to adapt behavior dynamically. In Python, this pattern can be implemented using class-based strategies, abstract base classes for formal interfaces, or even simple functions for lightweight scenarios, making it a versatile tool suitable for both simple and complex applications.



