Design Patterns with First-Class Functions
Notes based on Fluent Python, summarized and rephrased in my own words.[file:14]
Why many classic patterns shrink in Python
The classic Design Patterns book describes 23 object-oriented patterns. In a dynamic, high-level language like Python, many of these patterns become much simpler because:
- Functions are first-class values (they can be passed around like objects).
- Classes can be lightweight and created on the fly.
- Closures and higher-order functions let us capture behavior without ceremony.
Patterns like Strategy and Command that require multiple small classes in Java or C++ can often be expressed as a few functions plus some simple data structures.[file:14]
Strategy-like behavior using functions
The Strategy pattern encapsulates alternative algorithms behind a common interface so the caller can switch behavior at runtime.[file:14] In Python, the most direct way to do this is to:
- Represent each strategy as a plain function that accepts a context object.
- Pass the chosen function into the context.
Order, Customer, and LineItem
We can model a simple e-commerce checkout system with:
Customer: holds customer data such as name and fidelity points.LineItem: holds a product, quantity, and unit price, with atotal()method.Order: the context that applies a promotion strategy (if provided) when computing the final price.[file:14]
The key idea: Order does not know details of each promotion. It just calls a function stored in self.promotion (if any) and subtracts the discount from the total.[file:14]
Writing promotion functions
Each promotion is a separate function with the same signature, e.g. promo(order) -> discount:
- Fidelity promotion: if customer points are above a threshold, apply a percentage discount on the whole order.[file:14]
- Bulk item promotion: for each line item where quantity exceeds some threshold, apply an extra discount on that item total.[file:14]
- Large order promotion: if the number of distinct products in the cart passes a limit, discount the whole order.[file:14]
Because these are plain functions, we can:
- Add or remove promotions without touching the
Orderclass.[file:14] - Choose the promotion at runtime by passing a different function object.[file:14]
- Keep each rule small and focused, which makes testing easy.
Using different strategies
At call sites we simply pass the promotion function we want:
order = Order(customer, cart, promotion=fidelity_promo)
other = Order(customer, cart, promotion=bulk_item_promo)From the perspective of the Order class, a promotion is just a callable that takes the order and returns a discount value.[file:14] This is the essence of the Strategy pattern, expressed in a function-oriented way instead of small subclasses.
Discovering and combining strategies
Once strategies are functions, we can use Python’s introspection and data model to manage them in flexible ways, such as:
- Collecting all functions that follow a naming convention (e.g. all callables ending with
_promo). - Storing them in a list or dict and selecting them dynamically.
- Building a meta-promotion that picks the best discount by calling all strategies and taking the maximum.
This often removes the need for factory classes or complex registries. A simple list of functions and a loop can replace a whole family of small strategy classes.
Command-style behavior with callables
The Command pattern lets us treat operations as objects so they can be queued, logged, or undone.[file:14] In Python, a simple version of Command often looks like “just store some callables”.
A macro command
A macro command is a command that runs a list of other commands in sequence.[file:14] In Python we might implement it as a small class whose instances are callable:
class MacroCommand:
"""A command that executes a list of commands in order."""
def __init__(self, commands):
# store a copy so changes to the original sequence don't affect us
self.commands = list(commands)
def __call__(self):
for cmd in self.commands:
cmd()Key ideas:
- Each
cmdinself.commandscan be any callable with no arguments: a function, a bound method, a lambda, or an object with__call__.[file:14] - Calling a
MacroCommandinstance runs all stored commands in order.[file:14]
To create a macro, we just pass in a list of operations:
def open_file():
print("Opening file...")
def save_file():
print("Saving file...")
macro = MacroCommand([open_file, save_file])
macro() # runs both operationsFor more advanced scenarios (e.g. undo/redo, logging), you may still want small command classes that store extra state such as previous values. But for many tasks, plain callables and a tiny wrapper like MacroCommand are enough.[file:14]
When to use functions vs classes for patterns
Using functions and callables instead of full-blown classes is usually a good fit when:
- You only need to vary behavior, not store complex per-object state.
- The “objects” would otherwise be tiny classes with a single method.
- The pattern primarily coordinates behavior (e.g. Strategy, simple Command).
You may still prefer classes when:
- Each command or strategy carries substantial internal state beyond a few arguments.
- You need richer protocols (multiple methods) rather than a single
__call__. - You want to integrate with class-based features (inheritance hierarchies, ABCs).
The main takeaway is that Python’s first-class functions and callable objects make many patterns much simpler than in classical OO languages, without losing clarity.[file:14]