Card 06/ 09

ComparisonDifficulty: Intermediate2 min

Strategy Classes vs Passing a Function in Python

When a strategy has one method, a function is the strategy.

In Python a function can be passed as an argument, so a strategy with one method can be the function itself, with no interface and no class. This card puts the shipping strategies in that form beside the class form, then sets out what each form gives up and a rule for choosing.

py
import math
from collections.abc import Callable

Shipping = Callable[[float], int]


def standard(weight_kg: float) -> int:
    return 399 + 100 * math.ceil(weight_kg)


def express(weight_kg: float) -> int:
    return 899 + 200 * math.ceil(weight_kg)


def total_pence(goods_pence: int, weight_kg: float, shipping: Shipping) -> int:
    return goods_pence + shipping(weight_kg)


print(total_pence(4200, 2.5, standard))
print(total_pence(4200, 2.5, express))
print(total_pence(4200, 2.5, lambda weight_kg: 199))
OutputRan with python:3.12-slim (Python 3.12.13)
4899
5699
4399
Two named functions and a lambda fill the same slot.

The totals match the class version, £48.99 and £56.99, and the lambda, a function written inline, prices the collection point at £1.99 without a definition of its own. Shipping is now a type alias, Callable[[float], int], which a type checker uses to refuse a function with the wrong signature. The context is a function too, so the whole pattern has become one parameter.

The two forms compared
ConcernA strategy classA function
Code per strategyA class and a methodOne function or a lambda
ConfigurationHeld in fields, set in the constructorHeld in a closure, or bound with functools.partial
Several related operationsSeveral methods on one object, such as cost_pence and estimateSeveral functions that must be passed together
Name in errors and logsThe class nameThe function name, or <lambda>
InterfaceAn ABC or Protocol that a type checker enforcesA Callable alias that a type checker enforces

The decision rule follows from the third row. Start with a function when the strategy is one operation, which covers key= arguments, callbacks and most pricing rules. Move to a class when the strategy needs a second method that must stay consistent with the first, or configuration worth a name of its own. A shipping option that must report both a cost and a delivery estimate is two operations, and a class keeps them together.

Either way the pattern is still there: the context delegates the varying part of its work to something the caller chose, and only the amount of code needed to say so has changed.