Skip to content

Strategy Pattern

The Strategy Pattern lets you define a family of algorithms, encapsulate each one, and make them interchangeable at runtime. In Python, first-class functions replace the strategy classes required in traditional OOP implementations.

Key Insight: "Encapsulate what varies" — the algorithm varies, so pass it as a function parameter. The context class remains algorithm-agnostic.


Core Structure

class Context:
    def __init__(self, ..., strategy=None):
        self.strategy = strategy  # A function reference

    def do_something(self):
        if self.strategy:
            result = self.strategy(self)  # Call the strategy function
        return result

def strategy_a(context):
    # Algorithm A
    return result

def strategy_b(context):
    # Algorithm B
    return result

# Usage — swap strategies without touching Context
context = Context(..., strategy=strategy_a)

Example: E-Commerce Discount System

Data Structures

from decimal import Decimal
from typing import Callable, Optional, Sequence
from dataclasses import dataclass
from typing import NamedTuple


class Customer(NamedTuple):
    name: str
    fidelity: int  # loyalty points


class LineItem(NamedTuple):
    product: str
    quantity: int
    price: Decimal

    def total(self):
        return self.price * self.quantity


@dataclass(frozen=True)
class Order:
    customer: Customer
    cart: Sequence[LineItem]
    promotion: Optional[Callable[['Order'], Decimal]] = None  # ← strategy function

    def total(self) -> Decimal:
        return sum((item.total() for item in self.cart), start=Decimal(0))

    def due(self) -> Decimal:
        discount = self.promotion(self) if self.promotion else Decimal(0)
        return self.total() - discount

    def __repr__(self):
        return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'

The Order class accepts any callable as promotion. It doesn't know or care which strategy is used — it just calls it.

Strategy Functions

def fidelity_promo(order: Order) -> Decimal:
    """5% discount for customers with 1000+ fidelity points."""
    if order.customer.fidelity >= 1000:
        return order.total() * Decimal('0.05')
    return Decimal(0)


def bulk_item_promo(order: Order) -> Decimal:
    """10% discount on any line item with 20+ units."""
    discount = Decimal(0)
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * Decimal('0.1')
    return discount


def large_order_promo(order: Order) -> Decimal:
    """7% discount for orders with 10+ distinct products."""
    distinct_items = {item.product for item in order.cart}
    if len(distinct_items) >= 10:
        return order.total() * Decimal('0.07')
    return Decimal(0)


def new_customer_promo(order: Order) -> Decimal:
    """2% welcome discount for customers with fewer than 50 fidelity points."""
    if order.customer.fidelity < 50:
        return order.total() * Decimal('0.02')
    return Decimal(0)

Using Strategies

joe = Customer('John Doe', 0)      # no fidelity points
ann = Customer('Ann Smith', 1100)  # high fidelity

cart = [
    LineItem('banana', 4, Decimal('.5')),
    LineItem('apple', 10, Decimal('1.5')),
    LineItem('watermelon', 5, Decimal(5)),
]

# No discount
Order(joe, cart)
# <Order total: 17.00 due: 17.00>

# fidelity_promo — Joe has 0 points, no discount
Order(joe, cart, promotion=fidelity_promo)
# <Order total: 17.00 due: 17.00>

# fidelity_promo — Ann has 1100 points, 5% discount
Order(ann, cart, promotion=fidelity_promo)
# <Order total: 17.00 due: 16.15>

# bulk_item_promo — 30 bananas qualifies
banana_cart = [LineItem('banana', 30, Decimal('.5')), LineItem('apple', 10, Decimal('1.5'))]
Order(joe, banana_cart, promotion=bulk_item_promo)
# <Order total: 30.00 due: 28.50>

Adding New Strategies

No modifications to Order needed — just write a new function:

def flash_sale_promo(order: Order) -> Decimal:
    """15% discount on all orders during flash sale period."""
    return order.total() * Decimal('0.15')

# Plug it straight in
Order(joe, cart, promotion=flash_sale_promo)

This satisfies the Open/Closed Principle: open for extension, closed for modification.


Dynamic Strategy Selection

Because strategies are plain functions, they can be stored in lists and selected programmatically:

promos = [fidelity_promo, bulk_item_promo, large_order_promo]

def best_promo(order: Order) -> Decimal:
    """Apply whichever promotion gives the largest discount."""
    return max(promo(order) for promo in promos)

Order(ann, cart, promotion=best_promo)

Python vs Traditional OOP

=== "Pythonic (functions)"

```python
def fidelity_promo(order):
    if order.customer.fidelity >= 1000:
        return order.total() * Decimal('0.05')
    return Decimal(0)

order = Order(customer, cart, promotion=fidelity_promo)
```

=== "Traditional OOP (classes)"

```python
class Promotion:
    def discount(self, order): ...

class FidelityPromo(Promotion):
    def discount(self, order):
        if order.customer.fidelity >= 1000:
            return order.total() * Decimal('0.05')
        return Decimal(0)

order = Order(customer, cart, strategy=FidelityPromo())
```

The function-based approach is shorter, requires no inheritance hierarchy, and is easier to test each strategy in isolation.


When to Use the Strategy Pattern

Use it when:

  • You have multiple interchangeable algorithms for the same task
  • You want to swap algorithms at runtime
  • You want to eliminate large if/elif/else chains
  • New strategies should be addable without modifying existing code

Common applications:

Domain Strategies
E-commerce Discount calculations, shipping methods
Payments Credit card, PayPal, cryptocurrency
Sorting Bubble sort, quicksort, merge sort
Rendering HTML, PDF, JSON output
Compression ZIP, GZIP, BZIP2
Authentication Password, OAuth, SAML
Caching LRU, FIFO, LFU eviction

Skip it when:

  • You only have one or two algorithms that never change
  • The algorithms are trivial one-liners
  • Client code genuinely needs algorithm-specific knowledge

Summary

Concept Description
Context The class that delegates to a strategy (Order)
Strategy A callable that implements one algorithm (fidelity_promo)
Selection Pass strategy as a constructor or method argument
Extension Add new strategies as new functions — no class changes
Python advantage First-class functions eliminate the need for strategy classes

The one-line version: pass a function where behaviour varies; keep everything else the same.