The Strategy Pattern in Python

Learn how to use the strategy pattern in Python to make algorithms swappable at runtime, with practical examples using both classes and functions.

7 min read

The strategy pattern lets you select an algorithm at runtime. Instead of a class with many conditional branches for different behaviors, each behavior is a separate object or function. The code that uses the strategy does not know which one it has; it just calls a common interface.

This is one of the most useful patterns in Python because functions are first-class objects. A strategy can be a plain function, making the pattern lighter than in languages that require a class for every strategy. This article builds on the overview in Common Design Patterns in Python and the open-closed principle from SOLID.

Function-based strategies

When a strategy is a single operation, a plain function is the simplest approach. A shipping cost calculator accepts a strategy function and calls it for each order:

pythonpython
def fedex_shipping(order):
    return order.weight * 2.5 + 5
 
def ups_shipping(order):
    return order.weight * 2.0 + 3
 
def usps_shipping(order):
    return order.weight * 1.5 + 2
 
def calculate_shipping(order, strategy):
    return strategy(order)

The caller chooses the strategy based on user preference or configuration. Adding a new carrier means writing one function; no existing code changes:

pythonpython
cost = calculate_shipping(order, fedex_shipping)
# or
cost = calculate_shipping(order, ups_shipping)

This is the open-closed principle at work: the calculate_shipping function is closed for modification but open for extension through new strategy functions.

Class-based strategies with state

When a strategy needs configuration, use a class. The class stores its settings at initialization and exposes a single method that the caller invokes. A payment processor that supports multiple gateways uses class-based strategies:

pythonpython
class StripePayment:
    def __init__(self, api_key):
        self.api_key = api_key
 
    def pay(self, amount):
        print(f"Charging ${amount:.2f} via Stripe")
        return {"status": "ok", "gateway": "stripe"}
 
class PayPalPayment:
    def __init__(self, client_id, secret):
        self.client_id = client_id
        self.secret = secret
 
    def pay(self, amount):
        print(f"Charging ${amount:.2f} via PayPal")
        return {"status": "ok", "gateway": "paypal"}
 
 
class CheckoutService:
    def __init__(self, payment_strategy):
        self.payment = payment_strategy
 
    def process(self, order):
        return self.payment.pay(order.total)

Each strategy holds its own configuration. The CheckoutService receives a strategy and calls pay() without knowing which gateway is behind it. Testing is straightforward: pass a mock strategy that returns a canned response without hitting a real payment API.

Selecting strategies with a registry

Combine the strategy pattern with a registry to select strategies by name. A dictionary maps configuration values to factories, which is cleaner than an if-elif chain. Here is a registry that lets strategies register themselves with a decorator:

pythonpython
_payment_strategies = {}
 
def register_strategy(name):
    def decorator(cls):
        _payment_strategies[name] = cls
        return cls
    return decorator
 
def create_payment_strategy(name, **kwargs):
    strategy_class = _payment_strategies.get(name)
    if strategy_class is None:
        raise ValueError(f"Unknown payment strategy: {name}")
    return strategy_class(**kwargs)

Each strategy registers itself at definition time. The caller looks up and instantiates the right one from configuration, without an if-elif chain anywhere in the checkout code:

pythonpython
strategy = create_payment_strategy("stripe", api_key="sk_...")
checkout = CheckoutService(strategy)

This is the factory pattern from the factory pattern in Python combined with the strategy pattern. The factory selects which strategy to create; the strategy defines what to do.

Sorting with strategies

Python's sorted() function and list.sort() method accept a key parameter, which is a strategy function for determining sort order. This is the strategy pattern built into the language:

pythonpython
users = [{"name": "Ada", "age": 28}, {"name": "Grace", "age": 32}]
 
by_name = sorted(users, key=lambda u: u["name"])
by_age = sorted(users, key=lambda u: u["age"])

The key function receives each element and returns the value to sort by. Swapping the strategy swaps the sort order without changing the sorting algorithm itself.

When to use the strategy pattern

Use the strategy pattern when you have multiple algorithms for the same task and need to choose between them at runtime. Shipping, payment, compression, validation, and report generation all fit this pattern.

Do not use it when you have only two options that never change. An if-else with two branches is simpler and clearer than a strategy class hierarchy. The pattern earns its keep when the number of strategies grows or when strategies need independent testing.

The pattern pairs naturally with dependency injection and the factory pattern. Injecting a strategy through a constructor makes the dependency explicit and testable. Using a factory to select the strategy keeps creation logic separate from usage logic.

Rune AI

Rune AI

Key Insights

  • The strategy pattern lets you swap algorithms at runtime without changing the code that uses them.
  • Python functions are first-class objects and make excellent lightweight strategies.
  • Use classes for strategies that need configuration or maintain state.
  • The pattern eliminates long if-elif chains and makes adding new behaviors trivial.
RunePowered by Rune AI

Frequently Asked Questions

Should I use a class or a function for a strategy?

Use a function when the strategy is a single operation with no state. Use a class when the strategy needs configuration, maintains state between calls, or must implement a Protocol for type checking. Python's first-class functions make function-based strategies the simpler choice most of the time.

How is the strategy pattern different from dependency injection?

They are closely related. Dependency injection is the mechanism: you pass an object into a class through its constructor. The strategy pattern is the intent: you are passing a swappable algorithm. Every strategy is injected, but not every injected dependency is a strategy.

Conclusion

The strategy pattern is one of the most natural patterns in Python because functions are first-class objects. A strategy can be a function, a class with a call method, or a full class with a named method. Start with a function and add structure only when the strategy needs state or a formal interface.