An if/elif ladder choosing behavior, or a flag argument that swaps
algorithm mid-function:
def price(order, promo_kind):
if promo_kind == "bulk":
...
elif promo_kind == "large_order":
...
elif promo_kind == "loyalty":
...Every new algorithm edits this function, and nothing stops the branches from drifting apart in signature or behavior.
-
Name the signature. One type alias — e.g.
PromoRule = Callable[[Order], float]— is the whole "strategy interface";mypyenforces it from then on. -
Extract each branch into a function with that signature. The branch condition usually becomes the function's early
return 0.0(or equivalent "not applicable" value). -
Pass the strategy where the work happens. For a closed set, a plain parameter (
sorted(key=...)style) is finished — stop here. -
Register open families. When rules arrive over time (plugins, pricing, policies), a
StrategyRegistrymakes joining the family a decorator:from patterns.behavioral.strategy import StrategyRegistry promotion: StrategyRegistry[Order, float] = StrategyRegistry() @promotion.register def loyalty(order: Order) -> float: ... promotion.results(order) # every rule's answer, keyed by name promotion.get("loyalty") # or one by name — UnknownStrategyError otherwise
-
Make the selection policy explicit and tested. "Best discount wins" (
maxoverresults()) is a business rule — pin it with a test, next to tests for each individual strategy.
functools.partialparameterizes a strategy without a class:partial(percent_off, rate=0.05)is a new family member from an old recipe.- Registration by decoration puts a rule's membership at its definition
site — the same move Flask routes and
singledispatchuse. - A strategy needing state graduates to a callable object (
__call__) and slots into the same registry unchanged.
- Module-level registries are import-order state. A rule registers when
its module is imported; a rule nobody imports silently does not exist.
Import the rules module somewhere deliberate (the package
__init__). - Registries are shared across tests. Registering test-only strategies
mutates global state — register into a fresh
StrategyRegistryin tests, or clean up. - Signature drift. The alias only protects call sites that use it; annotate every strategy with the alias's exact shape.
- Comparing incomparable results.
best-style selection needs an ordering; keep strategy outputs plain (floats, tuples) or supply a key.
examples/promotions/ applies every step above to
checkout pricing — three registered rules, a best-rule engine, and a
comparison report:
uv run python -m patterns.behavioral.strategy.examples.promotions.main