What is it? #
The Strategy pattern puts each variation of an algorithm in its own object, all sharing one interface, so the caller can swap between them.
The problem it solves is the conditional that keeps growing. Shipping cost by method, ranking by criterion, compression by format — each new option means editing the same function.
With strategies, each option is a class. The context object holds one and delegates to it, without knowing which it has.
This is the most widely useful behavioural pattern, and in Python a strategy can often be a plain function rather than a class.
Think of it like this #
A navigation app with route options: fastest, shortest, avoid tolls, avoid motorways. The app does not contain four different navigation systems — it has one that accepts a routing strategy.
Adding "most scenic" means adding one strategy, not rewriting navigation.
Simple example #
Search results need different ranking depending on context: by relevance for a search box, by price for a listing page, by recency for a feed. The search service should not contain all three.
Code #
from abc import ABC, abstractmethod
from datetime import date
class RankingStrategy(ABC):
@abstractmethod
def sort_key(self, product: dict): ...
reverse: bool = False
class ByRelevance(RankingStrategy):
reverse = True
def sort_key(self, p): return p["score"]
class ByPriceAscending(RankingStrategy):
def sort_key(self, p): return p["price"]
class ByNewest(RankingStrategy):
reverse = True
def sort_key(self, p): return p["added_on"]
class ProductSearch:
"""The context: holds a strategy and delegates the varying part."""
def __init__(self, strategy: RankingStrategy):
self.strategy = strategy
def results(self, products: list[dict]) -> list[dict]:
return sorted(products, key=self.strategy.sort_key, reverse=self.strategy.reverse)
catalogue = [
{"name": "Keyboard", "price": 2499, "score": 0.7, "added_on": date(2026, 9, 1)},
{"name": "Mouse", "price": 899, "score": 0.9, "added_on": date(2026, 9, 20)},
{"name": "Monitor", "price": 12999, "score": 0.5, "added_on": date(2026, 8, 5)},
]
search = ProductSearch(ByRelevance())
print([p["name"] for p in search.results(catalogue)]) # Mouse, Keyboard, Monitor
search.strategy = ByPriceAscending() # swapped at runtime
print([p["name"] for p in search.results(catalogue)]) # Mouse, Keyboard, Monitor
# In Python, a strategy is often just a function
def by_discount(product): return product["price"] * -1
def results(products, key=by_discount):
return sorted(products, key=key)
# What this replaces
def results_with_conditionals(products, mode):
if mode == "relevance":
return sorted(products, key=lambda p: p["score"], reverse=True)
elif mode == "price":
return sorted(products, key=lambda p: p["price"])
elif mode == "newest":
return sorted(products, key=lambda p: p["added_on"], reverse=True)
# ... and this function is edited for every new sort option
When to use it
- several interchangeable ways to do one job
- the choice is made at runtime or by configuration
- each variation has its own rules or parameters
When NOT to use it
- two simple branches that will not grow
- a one-line lambda would express the whole variation
- the strategies need so much context that they end up coupled to the caller
How it works #
RankingStrategy defines the contract: how to produce a sort key and whether to reverse. That is the only thing that varies.
Each strategy is a small class holding one rule. ByPriceAscending needs nothing else to be understood or tested.
ProductSearch is the context. It owns the shared part — calling sorted — and delegates the varying part. It never names a specific strategy.
Swapping search.strategy mid-run shows the runtime flexibility. Inheritance could not do this: a subclass is fixed once created.
The function-based version underneath is the Pythonic shortcut. sorted(key=...) is itself a strategy interface, and passing a function is often all you need. Use classes when the strategy holds configuration, like a percentage or a threshold.
The conditional version at the bottom is what this replaces. It works, but it grows forever and every edit risks the existing branches.
One caveat worth naming: if strategies need a lot of context from the caller, the interface grows and the benefit shrinks. Keep the input to a strategy small and well defined.
Real-world use #
Sorting and ranking, pricing and discount rules, tax calculation by region, compression and serialisation formats, authentication methods, retry policies — all are commonly implemented as strategies.
Machine learning libraries expose optimisers and loss functions as strategies you pass in. Web frameworks accept a session backend or a cache backend the same way.
Feature flags pair naturally with the pattern: a flag chooses which strategy gets injected, so an experiment is a configuration change rather than a code change.
In practice this is the pattern most often reached for when a conditional starts growing, and it is also the usual replacement when an inheritance hierarchy turns out to be about behaviour rather than identity.
Common mistakes #
- Giving each strategy a different method signature, so they are not interchangeable.
- Creating strategies that need five pieces of caller context, coupling them tightly.
- Applying the pattern to two branches that will never grow.
- Keeping a conditional inside the context to pick the strategy, which reintroduces the problem.
- Using a class where a plain function would express the whole variation.
Practice #
Write three ShippingCost strategies — flat rate, weight-based, and free-over-threshold — and a Checkout context that uses one. Then add a fourth strategy and confirm Checkout needed no changes. Finally, rewrite one strategy as a plain function and compare.