Card 05/ 08
Pattern Classes vs Python Functions and Modules
With first-class functions, Strategy becomes a parameter and Iterator a generator.
Several of the 23 patterns exist to work around something the languages of 1994 could not do, and in Python those patterns shrink or disappear. The clearest case is a function: Python can pass one as an argument, store it in a list or return it, which is called a first-class function. A pattern whose only job is to carry one method around can then be replaced by the function itself. This card shows which patterns a language feature absorbs, and what you give up when you use the feature instead.
In 1996 Peter Norvig presented "Design Patterns in Dynamic Languages", and found that 16 of the 23 become simpler or invisible in Lisp and Dylan. He grouped them by the language feature that does the pattern's work.
| Feature Norvig named | Patterns it absorbs | In Python |
|---|---|---|
| First-class types | Abstract Factory, Flyweight, Factory Method, State, Proxy, Chain of Responsibility | Yes. A class is an object, so it can be passed and stored like any value. |
| First-class functions | Command, Strategy, Template Method, Visitor | Yes. Functions, lambdas and functools.partial. |
| Macros | Interpreter, Iterator | No macros. Generators do Iterator's work instead. |
| Method combination | Mediator, Observer | Not built in; a list of callables covers most of Observer. |
| Multimethods | Builder | No. Keyword arguments with defaults cover most of Builder. |
| Modules | Facade | Yes. A module is a natural facade over a package. |
Here is Strategy both ways. The first half is the class form: a Discount protocol and one class that implements it. The second half passes a function where the object was, and then a lambda, a function written inline.
from collections.abc import Callable
from typing import Protocol
class Discount(Protocol):
def apply(self, price: float) -> float: ...
class TenPercentOff:
def apply(self, price: float) -> float:
return price * 0.9
class Checkout:
def __init__(self, discount: Discount) -> None:
self.discount = discount
def total(self, prices: list[float]) -> float:
return round(sum(self.discount.apply(p) for p in prices), 2)
print("class:", Checkout(TenPercentOff()).total([20.0, 5.0]))
def ten_percent_off(price: float) -> float:
return price * 0.9
def total(prices: list[float], discount: Callable[[float], float]) -> float:
return round(sum(discount(p) for p in prices), 2)
print("function:", total([20.0, 5.0], ten_percent_off))
print("lambda:", total([20.0, 5.0], lambda price: price - 1))class: 22.5
function: 22.5
lambda: 23.0The class and the function give the same total, and the lambda, a different rule that takes one off each price, fits the same slot. The function form needs no protocol and no class. The pattern has not gone away: total is still a context that delegates to an interchangeable algorithm. What disappeared is the ceremony that Java and C++ needed to pass behaviour around.
The class formwhen the strategy carries state or several methods
A class can hold configuration, such as a percentage set once in its constructor, and can offer more than one method, such as apply and describe. Its name appears in stack traces and in a type checker's errors.
The function formwhen the strategy is one operation
A function is the whole strategy in one definition, and any matching function or lambda fits without declaring anything. It cannot carry several related methods, and it grows awkward once it needs configuration beyond what a closure holds.
The rule of thumb is to start with the function, and turn it into a class when it needs a second method or state worth naming. Knowing the pattern still matters in Python: it is how you recognise that a key= argument or a callback is a strategy, and what that design lets you change.
← → move through sections and cards