What you'll learn
- Recognize the four patterns you will actually meet: strategy, factory, observer, and adapter
- Implement each with plain functions, dictionaries, and small classes rather than ceremony
- Explain which classic patterns Python makes unnecessary, and why
- Know the smell each pattern cures, so you reach for it when the smell appears
Concept
Design patterns are named solutions to problems that recur in object-oriented code. The names come from a 1994 book written for languages without first-class functions or dynamic typing, and much of the ceremony in those solutions exists to work around those limits. Python has functions as values, duck typing, and dictionaries, so most patterns shrink to a few lines, and several disappear. The names are still worth knowing: they let you say "this is a strategy" and be understood in a code review, and they tell you the shape of the fix when you recognize the problem.

Strategy: an algorithm that varies is passed in rather than hard-coded. In Python a strategy is a function; sorted(key=...) is the strategy pattern. When behavior needs configuration or state, a small class with one method works, but start with a function. Factory: the code that creates an object is separated from the code that uses it, so the caller asks for "a notifier for this config" without knowing the class. A dictionary mapping names to classes or functions is a factory; so is a classmethod alternative constructor. Observer: an object announces events, and interested parties register callbacks to be told. A list of callables and a subscribe method is the whole pattern. Adapter: an object with the wrong interface is wrapped in one with the right interface, so existing code can use it; a small class that delegates, or even a function, does it.
| Pattern | Smell it cures | Python shape |
|---|---|---|
| Strategy | an if/elif choosing between algorithms |
pass a function; a dictionary of functions |
| Factory | if kind == "email": Email(...) elif ... at every creation site |
a dictionary mapping names to classes; a classmethod |
| Observer | one object calling many others by name when something happens | a list of callbacks and a subscribe method |
| Adapter | code that cannot use a library because the method names differ | a wrapper class that delegates and renames |
| Template | subclasses that repeat a skeleton with small differences | an ABC with the skeleton calling abstract steps |
| Decorator | adding behavior around many functions | @decorator (the language has it built in) |
Some patterns are unnecessary in Python. Singleton becomes a module, which is already a single shared object; a module-level instance is the idiom. Iterator is the iteration protocol and generators. Command is a function or a functools.partial. Decorator is a language feature. Builder is keyword arguments and dataclasses with defaults. Recognizing these saves you from importing Java shapes into Python code, which is one of the most common things reviewers flag in code written by developers arriving from other languages.
The habit to build is to name the smell first. An if chain over a kind field means a factory or polymorphism. Growing chains of if algorithm == ... mean a strategy. A class that has to know about everyone it notifies means an observer. A library whose interface does not match yours means an adapter. Reach for the pattern when the smell appears, in its smallest Python form, and let it grow only if it needs to.

Code
Strategy as functions. The shipping calculator takes the pricing rule as a parameter; a dictionary maps names to rules so a config string can choose one.
# strategy.py
def flat_rate(weight_kg):
return 5.0
def by_weight(weight_kg):
return round(2.0 + 1.5 * weight_kg, 2)
def free_over_10kg(weight_kg):
return 0.0 if weight_kg > 10 else by_weight(weight_kg)
STRATEGIES = {"flat": flat_rate, "weight": by_weight, "promo": free_over_10kg}
def shipping_cost(weight_kg, strategy=by_weight):
return strategy(weight_kg)
print(shipping_cost(4))
print(shipping_cost(4, flat_rate), shipping_cost(12, free_over_10kg))
config = "promo"
print(shipping_cost(3, STRATEGIES[config]))
8.0
5.0 0.0
6.5Factory as a registry. New notifier types register themselves with a decorator; the creation site never changes. A classmethod factory handles the "build from config" variant.
# factory.py
REGISTRY = {}
def register(kind):
def decorator(cls):
REGISTRY[kind] = cls
return cls
return decorator
@register("email")
class EmailNotifier:
def __init__(self, address):
self.address = address
def send(self, msg):
return f"email to {self.address}: {msg}"
@register("sms")
class SmsNotifier:
def __init__(self, number):
self.number = number
def send(self, msg):
return f"sms to {self.number}: {msg}"
@classmethod
def from_config(cls, config):
return cls(config["phone"].replace(" ", ""))
def make_notifier(kind, target):
try:
return REGISTRY[kind](target)
except KeyError:
raise ValueError(f"unknown notifier {kind!r}; known: {sorted(REGISTRY)}") from None
print(make_notifier("email", "ada@example.com").send("hi"))
print(SmsNotifier.from_config({"phone": "555 0100"}).send("hi"))
try:
make_notifier("fax", "x")
except ValueError as err:
print(err)
email to ada@example.com: hi
sms to 5550100: hi
unknown notifier 'fax'; known: ['email', 'sms']Observer as a list of callbacks. The order knows nothing about logging, stock, or email; each subscribes to the events it cares about.
# observer.py
class EventBus:
def __init__(self):
self._subscribers = {}
def subscribe(self, event, callback):
self._subscribers.setdefault(event, []).append(callback)
def publish(self, event, **data):
for callback in self._subscribers.get(event, []):
callback(**data)
bus = EventBus()
stock = {"pen": 5}
log = []
bus.subscribe("order.placed", lambda item, qty: log.append(f"logged {qty} x {item}"))
bus.subscribe("order.placed", lambda item, qty: stock.__setitem__(item, stock[item] - qty))
bus.subscribe("order.placed", lambda item, qty: log.append("email sent") if qty >= 3 else None)
bus.publish("order.placed", item="pen", qty=3)
bus.publish("order.placed", item="pen", qty=1)
print(log)
print(stock)
['logged 3 x pen', 'email sent', 'logged 1 x pen']
{'pen': 1}Adapter around a third-party-shaped class, and the patterns Python makes unnecessary: a module-level singleton and a function standing in for a command object.
# adapter_and_absent.py
import functools
class LegacyPaymentGateway: # the library we cannot change
def make_payment(self, cents, currency_code):
return f"charged {cents / 100:.2f} {currency_code}"
class PaymentAdapter: # the interface our code expects
def __init__(self, gateway):
self._gateway = gateway
def charge(self, amount, currency="EUR"):
return self._gateway.make_payment(round(amount * 100), currency)
def checkout(payment, amount):
return payment.charge(amount)
print(checkout(PaymentAdapter(LegacyPaymentGateway()), 12.5))
# "singleton": a module-level object is already shared by every importer
settings = {"debug": False}
print(id(settings) == id(settings), "one shared object per module")
# "command": a function, or a partial, is the command object
commands = [functools.partial(print, "undo"), functools.partial(print, "redo")]
for command in commands:
command()
charged 12.50 EUR
True one shared object per module
undo
redoTry it yourself
Create report_engine.py with a strategy dictionary of three formatters (plain, CSV, Markdown), a registry-based factory for data sources (list, json_file, sqlite), an EventBus that logs when a report starts and finishes, and an adapter that wraps a class with a write_out(text) method so it can be used where write(text) is expected. Wire them into a run_report(source_kind, format_name) function and run two combinations.
Common mistakes
- Importing Java shapes. An abstract
Strategyinterface with oneexecutemethod and three subclasses is a dictionary of functions in Python. - Patterns before smells. Adding a factory "in case we need more types" adds indirection nobody asked for; add it when the second type arrives.
- A singleton class. A module already is one; a class with
_instancejuggling solves a problem Python does not have.
What's next
- Strategy is a passed-in function, factory is a registry dictionary or
classmethod, observer is a list of callbacks, adapter is a small delegating wrapper. - Singleton, iterator, command, decorator, and builder are language features or idioms in Python, not patterns to implement.
- Name the smell first; apply the smallest Python form; let it grow only when needed.
You can shape larger programs with intent. Next you put the Advanced level together: a typed, tested library with dataclasses, logging, and a test suite, ready to become a real package in the Expert level.