Classic OOP design patterns often become simpler or disappear entirely in Python because functions are first-class objects. The Strategy and Command patterns reduce to plain functions.
In this chapter you will learn to:
Recognize the Strategy pattern in its classic OOP form.
Refactor it into a function-oriented form using first-class functions.
Build a discoverable strategy registry with a decorator.
Recognize the Command pattern and replace it with callable instances or plain functions.
22.1 Strategy, the classic way
The textbook Strategy pattern uses an abstract base class, one subclass per algorithm, and a context that holds an instance:
Walking through the parts of the textbook pattern:
Promotion(ABC) is the strategy interface — an abstract base class with one method (discount) that subclasses must implement. The @abstractmethod decorator stops anyone from instantiating Promotion directly.
FidelityPromo and BulkItemPromo are concrete strategies — one class per algorithm, each implementing discount its own way.
Order is the context. It holds a promotion and delegates the discount computation to it via self.promotion.discount(self). Order doesn’t care which strategy it has, only that whatever it has implements the interface.
The call Order(ann, cart, FidelityPromo()) instantiates the strategy and hands it in.
This is the pattern as it appears in Design Patterns (Gamma et al., 1994) — the form you’d write in Java or C++. Notice the boilerplate: each strategy is a class, and each class wraps a single function (discount).
flowchart LR
subgraph OOP["OOP Strategy"]
P["Promotion (ABC)"]
P --> F1["FidelityPromo"]
P --> F2["BulkItemPromo"]
P --> F3["LargeOrderPromo"]
end
subgraph FN["Function Strategy"]
f1["fidelity_promo"]
f2["bulk_item_promo"]
f3["large_order_promo"]
M["best_promo = max(p(order) for p in promos)"]
end
OOP -.collapses to.-> FN
22.2 Strategy as plain functions
In Python a function is a callable. The whole class hierarchy collapses to one function per strategy:
def fidelity_promo(order): rate = Decimal("0.05")return order.total() * rate if order.customer.fidelity >=1000else Decimal(0)def bulk_item_promo(order): d = Decimal(0)for item in order.cart:if item.quantity >=20: d += item.quantity * item.price * Decimal("0.1")return ddef large_order_promo(order): distinct_items = {item.product for item in order.cart}return order.total() * Decimal("0.07") iflen(distinct_items) >=10else Decimal(0)class OrderF:def__init__(self, customer, cart, promotion=None):self.customer = customerself.cart =list(cart)self.promotion = promotiondef total(self):returnsum(item.quantity * item.price for item inself.cart)def due(self): discount = Decimal(0) ifself.promotion isNoneelseself.promotion(self)returnself.total() - discountOrderF(ann, cart, fidelity_promo).due()
Decimal('16.150')
Walking through what changed and what didn’t:
Each strategy is now a plain def. fidelity_promo(order) does the same arithmetic the class-based FidelityPromo.discount did — but there’s no class wrapping it.
OrderF differs from Order in one line: self.promotion(self) instead of self.promotion.discount(self). We’re calling the promotion directly, because functions are callable.
The call site OrderF(ann, cart, fidelity_promo) passes the function itself — no (), no instantiation. Compare to the OOP version that needed FidelityPromo().
Same answer; one-third the code; no abstract classes; no instantiation overhead. The general lesson: when a “class” exists only to hold one method, in Python that class can simply be the method.
A “best-of” combinator is a one-liner:
promos = [fidelity_promo, bulk_item_promo, large_order_promo]def best_promo(order):returnmax(promo(order) for promo in promos)OrderF(ann, cart, best_promo).due()
Decimal('16.150')
22.3 Discovering strategies automatically
promos = [...] is a hand-maintained list. Two ways to make it self-updating.
By inspection — find every function in a module whose name ends _promo:
import inspectimport promotions_modulepromos = [func for name, func in inspect.getmembers(promotions_module, inspect.isfunction)if name.endswith("_promo") and name !="best_promo"]
By decorator registration — even cleaner:
from collections.abc import CallablePromotion = Callable[["OrderF"], Decimal]promos: list[Promotion] = []def promotion(promo: Promotion) -> Promotion: promos.append(promo)return promo@promotiondef fidelity_promo(order): rate = Decimal("0.05")return order.total() * rate if order.customer.fidelity >=1000else Decimal(0)@promotiondef bulk_item_promo(order): d = Decimal(0)for item in order.cart:if item.quantity >=20: d += item.quantity * item.price * Decimal("0.1")return ddef best_promo(order):returnmax(promo(order) for promo in promos)[p.__name__for p in promos]
['fidelity_promo', 'bulk_item_promo']
Walking through the registry trick:
Promotion = Callable[["OrderF"], Decimal] is a type alias for “a function that takes an OrderF and returns a Decimal.” It documents the protocol the strategies must satisfy. (The forward reference "OrderF" is in quotes because the class is defined in a different cell.)
promos: list[Promotion] = [] is the registry — initially empty, populated as decorators run.
promotion(promo) is the decorator. It appends promo to promos for its side effect, then returns promounchanged. That last detail matters: a decorator that returns the original function leaves the call site behavior alone — we get the registration plus the function callable as before.
@promotion above def fidelity_promo is sugar for fidelity_promo = promotion(fidelity_promo). Since promotion returns its argument, the binding fidelity_promo still names the same function — but promos now contains it.
The decorator runs at import time (see Chapter 21), so adding a new strategy is one @promotion line. The best_promo function picks it up automatically — no list to maintain.
The general pattern: a registry decorator is a function that returns its argument and keeps a side-effect record of every function it sees. It’s the universal recipe for plugin-style discovery.
22.4 The Command pattern
The classic Command pattern wraps each action in an object with an execute method, so commands can be queued, logged, or undone. In Python, any callable already meets that interface — you don’t need a class.
class MacroCommand:"""Run a sequence of commands in order."""def__init__(self, commands):self.commands =list(commands)def__call__(self):for command inself.commands: command()def open_file(): print("opening file")def save_file(): print("saving file")def close_app(): print("closing app")macro = MacroCommand([open_file, save_file, close_app])macro()
opening file
saving file
closing app
Walking through the collapse:
Each “command” is a plain function (open_file, save_file, close_app). In Java each would be a class implementing Command.execute().
MacroCommand.__init__ stores the list of commands. list(commands) copies — the macro owns its sequence even if the caller mutates theirs later.
__call__(self) makes the instance callable (see Chapter 19): macro() invokes MacroCommand.__call__(macro), which loops and calls each command in turn.
Calling command() works uniformly because every “command” is just a callable — function, lambda, class with __call__, or another MacroCommand.
The general pattern: where Java needs an interface (Command { void execute(); }) and one class per action, Python lets the callable protocol play that role. Replace command objects with callables; replace strategy objects with strategy functions. The pattern doesn’t disappear — it becomes idiomatic Python.
TipWhy this matters
The Strategy and Command design patterns encode behavior in classes because in languages like Java, functions are not first-class objects. In Python, behavior is a function. Replace strategy objects with strategy functions. Replace command objects with callables. The pattern disappears — you’re left with cleaner code.
22.5 Build: a CLI command dispatcher with @command
A common pattern in tooling: a script exposes several subcommands (mytool greet alice, mytool tally), and dispatch happens by name. The textbook OOP version is the Command pattern with a base class and one subclass per command. In Python, a registry dict + a decorator is the whole thing.
Step 1: a basic registry decorator. A dict keyed by function name; @command registers and returns the function unchanged so it can still be called directly:
COMMANDS[func.__name__] = func is the side effect; return func keeps the call site behavior unchanged. Both greet("Alice") and COMMANDS["greet"]("Alice") work — the registration is additive. This is the same @register pattern from the decorators chapter, applied to dispatch.
Step 2: parameterise to allow custom names. Some commands want a friendlier name than the Python identifier — bye instead of farewell. A parameterised decorator factory adds the configuration:
@command() (with empty parens) calls the factory first; command() returns decorate, which then wraps greet. name or func.__name__ falls back to the function’s own name when the caller doesn’t override — the truthy-default idiom from chapter 2.
Step 3: a dispatch function that calls the right command by name. Looking up by string + raising KeyError for unknown commands gives you a usable mini-CLI in five lines:
dispatch(name, *args) looks up the function in COMMANDS, then calls it with the forwarded args. The error message includes sorted(COMMANDS) so a typo gets a helpful “did you mean one of these?” trace. In a real CLI you’d plug argparse (chapter 8) on top: take the first positional arg as the command name, forward the rest.
The build is the chapter in motion: each command is a plain function, not a Command-pattern subclass; registration is a side effect of import-time decorator runs; dispatch is dict lookup; and the whole thing is twenty lines without a single ABC.
22.6 Exercises
Add a strategy. Write a weekend_promo function that gives 5% off if the order is placed on a Saturday or Sunday. Register it with @promotion and confirm best_promo finds it without other changes.
Strategy with state. Suppose a strategy needs configuration (a discount rate, an enabled flag). How would you represent it as a function? As a class with __call__? Compare.
Discoverable Command. Apply the registry-decorator idea from @promotion to commands: a @command decorator that registers each command in a global list. Implement run_all().
Where the OOP version wins. Name a real situation in which the class-based Strategy pattern is better than the function form. (Hint: think about state, inheritance, or testing.)
MacroCommand undo. Add an undo method to MacroCommand that runs the commands in reverse. Then convert MacroCommand itself to a plain function — what’s lost?
22.7 Summary
This chapter closes Part II. Many of the Gang of Four patterns are workarounds for languages without first-class functions; in Python, those patterns shrink or vanish. The lesson is not that classes are bad — it’s that you should reach for the simplest tool that solves the problem, and in Python that’s often a function.
Part III turns to the case where classes do earn their keep. We start in Chapter 23 with the canonical Vector2d: a single class that becomes printable, hashable, formattable, and slottable as we add the right special methods.