
Post #5’s CONVERSIONS dictionary — mapping a menu choice to the function that should handle it — was never named as such at the time, but it is a textbook example of a genuine, well-known design pattern: the Strategy pattern, selecting an algorithm to use at runtime rather than hardcoding one specific choice. Post #6 flagged that “favor composition over inheritance” deserved deeper treatment than a single paragraph could give it, and deferred that treatment to this post specifically.
Design patterns are named, reusable solutions to recurring software design problems — a shared vocabulary that lets developers say “this is a Factory” or “that’s an Observer” and immediately communicate a whole structural idea without re-explaining it from scratch. This post covers three patterns Python developers genuinely reach for — Strategy, Factory, and Observer — alongside the deeper composition-versus-inheritance discussion promised earlier, and, just as importantly, an honest accounting of when Python’s dynamic nature makes the traditional, heavily class-based version of these patterns more ceremony than the problem actually needs.
The Mental Model: Named Solutions, Not Rules to Follow
Design patterns originated largely from observing recurring structures in object-oriented codebases, most famously catalogued in the 1994 “Gang of Four” book — patterns developed primarily in the context of languages like C++ and Java, where functions are not first-class values the way they are in Python (a distinction Post #13 covered directly). This context matters: several patterns that require substantial class hierarchies in those languages can be expressed far more simply in Python, precisely because Python already treats functions as ordinary, storable, passable values.
The right way to hold design patterns: as a vocabulary for describing structures you might reach for, not a checklist of things “proper” code must include. A pattern applied because it genuinely solves the problem at hand is valuable. A pattern applied because a codebase “should have design patterns in it” is exactly the kind of over-engineering this post explicitly warns against later.
Strategy Pattern: Selecting Behavior at Runtime
The Strategy pattern’s core idea: define a family of interchangeable algorithms, and let the caller select which one to use without the surrounding code needing to know the specifics of each option.
The Traditional, Class-Based Version
from abc import ABC, abstractmethod
class ConversionStrategy(ABC):
@abstractmethod
def convert(self, value: float) -> float:
...
class MilesToKmStrategy(ConversionStrategy):
def convert(self, value: float) -> float:
return value * 1.60934
class FahrenheitToCelsiusStrategy(ConversionStrategy):
def convert(self, value: float) -> float:
return (value - 32) * 5 / 9
def perform_conversion(strategy: ConversionStrategy, value: float) -> float:
return strategy.convert(value)
result = perform_conversion(MilesToKmStrategy(), 10)
ABC (Abstract Base Class) and @abstractmethod — new tools here — enforce that any ConversionStrategy subclass must implement convert(), or Python refuses to let it be instantiated at all. This is the traditional, faithful expression of the Strategy pattern as originally catalogued — and it is genuinely more ceremony than the equivalent Python code needs.
Post #5’s Version, Recognized for What It Is
CONVERSIONS = {
"1": ("Miles to Kilometers", miles_to_km),
"2": ("Kilometers to Miles", km_to_miles),
"3": ("Fahrenheit to Celsius", fahrenheit_to_celsius),
}
This achieves exactly the same runtime algorithm selection as the class hierarchy above — a specific behavior chosen dynamically based on a key — using four lines instead of roughly twenty, and without needing ABC, abstractmethod, or any class definitions at all. This works specifically because Python’s first-class functions (Post #13) already provide “a swappable piece of behavior” without needing an object wrapping it. The honest lesson here: when the “strategies” being selected between are simple functions with a matching signature, a plain dictionary of functions is very often the more idiomatic Python solution — reach for the full class-based version specifically when each strategy genuinely needs to carry its own internal state or configuration beyond what a single function captures.
Factory Pattern: Centralizing Object Creation
The Factory pattern centralizes the logic for creating an object, so calling code does not need to know the specific class being instantiated — only what it asked for.
class LengthConverter:
def convert(self, value: float) -> float:
return value * 1.60934
class TemperatureConverter:
def convert(self, value: float) -> float:
return (value - 32) * 5 / 9
def converter_factory(conversion_type: str):
"""Return the right converter instance without the caller needing to know the class."""
converters = {
"length": LengthConverter,
"temperature": TemperatureConverter,
}
converter_class = converters.get(conversion_type)
if converter_class is None:
raise ValueError(f"Unknown conversion type: {conversion_type}")
return converter_class()
converter = converter_factory("length")
result = converter.convert(10)
The calling code never writes LengthConverter() directly — it asks the factory for “length” and receives an appropriately configured object back, without needing to import or know about every specific converter class that exists. This genuinely earns its place when object construction involves real decision logic or configuration beyond a simple one-to-one class lookup — for the simple case shown here, note that this factory itself is, structurally, extremely close to the Strategy dictionary above, just returning instances rather than calling functions directly.
Observer Pattern: One Event, Many Reactions
The Observer pattern lets multiple independent pieces of code react to something happening, without the code that triggers the event needing to know anything about who is listening or what they will do.
class ConversionEvent:
def __init__(self):
self._observers: list[callable] = []
def subscribe(self, observer: callable) -> None:
self._observers.append(observer)
def notify(self, conversion_data: dict) -> None:
for observer in self._observers:
observer(conversion_data)
def log_conversion(data: dict) -> None:
print(f"[LOG] {data['input']} → {data['output']} ({data['label']})")
def save_to_history_file(data: dict) -> None:
# Reuses the JSON persistence pattern from Post #9
with open("conversion_history.json", "a", encoding="utf-8") as f:
f.write(json.dumps(data) + "\n")
def alert_on_large_value(data: dict) -> None:
if data["input"] > 1000:
print(f"⚠️ Unusually large conversion: {data['input']}")
conversion_event = ConversionEvent()
conversion_event.subscribe(log_conversion)
conversion_event.subscribe(save_to_history_file)
conversion_event.subscribe(alert_on_large_value)
# Anywhere a conversion happens:
conversion_event.notify({"input": 1500, "output": 2414.01, "label": "Miles to Kilometers"})
# All three observers run automatically, independently, in the order they subscribed
The unit converter’s core conversion logic never needs to know that logging, history-saving, or large-value alerting exist at all — each concern is registered independently, and adding a fourth observer (an email notification, a Slack message, anything) requires zero changes to the conversion logic itself, only one more subscribe() call. This decoupling — the thing that happens versus the things that react to it — is the entire value the Observer pattern provides.
Composition Over Inheritance, Revisited in Depth
Post #6 introduced the “is-a versus has-a” test and promised deeper treatment here. Consider a notification system that initially seems like a natural fit for inheritance:
class Notifier:
def send(self, message: str) -> None:
raise NotImplementedError
class EmailNotifier(Notifier):
def send(self, message: str) -> None:
print(f"📧 Emailing: {message}")
class SMSNotifier(Notifier):
def send(self, message: str) -> None:
print(f"📱 Texting: {message}")
This works fine — until a genuinely common real requirement arrives: a user wants notifications sent via both email and SMS simultaneously. Inheritance offers no clean way to combine two sibling classes like this; you cannot cleanly make one class inherit from two unrelated notification types and get sensible combined behavior. Composition solves this naturally, because it does not require the combination to fit into a single rigid class hierarchy at all:
class MultiChannelNotifier:
def __init__(self, channels: list[Notifier]):
self.channels = channels
def send(self, message: str) -> None:
for channel in self.channels:
channel.send(message)
notifier = MultiChannelNotifier([EmailNotifier(), SMSNotifier()])
notifier.send("Your conversion is ready")
# 📧 Emailing: Your conversion is ready
# 📱 Texting: Your conversion is ready
MultiChannelNotifier has a list of notifiers — it is not, itself, a kind of notifier extending some shared base in a fixed hierarchy. This is precisely why composition is generally more flexible than inheritance for combining independent behaviors: new channels can be added by writing one more small class and passing an instance into the list, at runtime, with zero changes to any existing class — inheritance’s fixed, compile-time-like hierarchy offers no equivalent flexibility for combining sibling behaviors this freely.
The deeper principle, stated directly: inheritance creates a rigid relationship, fixed the moment a class is defined — a Manager is permanently, structurally a kind of Employee (Post #6), which is entirely appropriate for a genuine “is-a” relationship. Composition creates a flexible relationship, assembled at runtime from independent, swappable pieces — appropriate whenever the relationship is “has-a,” “uses-a,” or “can be combined with,” which describes the majority of relationships in real software far more often than “is-a” does.
Real-World Use Cases
Strategy: Payment processing choosing between different payment providers, sorting algorithms selected based on data characteristics, or — exactly as this post demonstrated — a dispatch dictionary routing a request to the appropriate handler function.
Factory: Database connection creation that varies by configured backend (PostgreSQL, SQLite, MySQL), UI component creation that varies by platform, or any situation where “which specific class to instantiate” is itself a decision made at runtime rather than hardcoded.
Observer: Event-driven systems broadly — a user action triggering multiple independent reactions (logging, analytics, notifications, cache invalidation) without those reactions needing to be hardcoded into the triggering code itself; GUI frameworks and web frameworks both rely on this pattern extensively for handling user interactions.
Composition over inheritance: Nearly any situation involving optional or combinable capabilities — exactly the multi-channel notification example — where forcing a single fixed inheritance hierarchy would be more rigid than the actual problem requires.
Common Mistakes and Gotchas
⚠️ Mistake 1: Over-engineering — reaching for a formal pattern where simple code suffices
# Overkill for two straightforward operations
class AdditionStrategy:
def execute(self, a, b): return a + b
class SubtractionStrategy:
def execute(self, a, b): return a - b
# Simpler, equally correct, easier to read
def calculate(op: str, a: float, b: float) -> float:
if op == "add":
return a + b
return a - b
The class ceremony above adds ceremony without adding real flexibility for a problem this small — this is “pattern-itis,” applying a named structure because it exists, not because it solves anything the simpler alternative does not.
⚠️ Mistake 2: Cargo-culting Java/C++ patterns into Python without questioning whether Python’s dynamic features already solve the problem more simply The Strategy pattern’s class hierarchy is the correct, often necessary solution in languages without first-class functions. In Python, as this post demonstrated directly, a dictionary of plain functions frequently achieves the identical outcome with a fraction of the code — recognize when a pattern’s traditional implementation is solving a language limitation that Python simply does not have.
⚠️ Mistake 3: Reaching for inheritance to combine independent behaviors Covered in depth above — inheritance’s single, fixed hierarchy handles genuine “is-a” relationships well and combinable, optional behaviors poorly. When you find yourself wanting a class to inherit from two unrelated things to get combined behavior, that is a strong signal composition is the better fit.
⚠️ Mistake 4: Building an Observer system with tightly coupled observers An observer that reaches back into the object that triggered it, modifying its internal state directly, defeats the entire purpose of the pattern’s decoupling — observers should react to the data they are given, independently, without depending on or modifying the source’s internals.
⚠️ Mistake 5: Applying a pattern’s name without applying its actual intent
Naming a class UserFactory does not automatically make it a well-designed Factory if it does not actually centralize any meaningful creation logic — the pattern’s value comes from what it structurally does, not from matching a familiar name.
Performance Note
Design patterns, correctly applied, are primarily about code organization, flexibility, and maintainability — not performance, and rarely have a meaningful direct performance cost or benefit compared to equivalent, less-structured code. The dictionary-based Strategy pattern shown in this post is, if anything, marginally faster than the class-based version (a direct dictionary lookup versus method resolution through a class hierarchy), though this difference is negligible for virtually any realistic use case — Post #17’s profiling discipline applies here exactly as everywhere else: choose based on actual code clarity and flexibility needs, and only investigate performance if profiling data specifically indicates a pattern-related bottleneck, which is rare in practice.
Quick Reference
# Strategy — dictionary version (idiomatic Python for simple function strategies)
STRATEGIES = {"add": lambda a, b: a + b, "sub": lambda a, b: a - b}
result = STRATEGIES["add"](3, 4)
# Strategy — class version (when strategies need their own state)
class Strategy(ABC):
@abstractmethod
def execute(self, *args): ...
# Factory — centralizes object creation
def create_thing(thing_type: str):
registry = {"a": ClassA, "b": ClassB}
return registry[thing_type]()
# Observer — one event, many independent reactions
class EventEmitter:
def __init__(self):
self._subscribers = []
def subscribe(self, fn):
self._subscribers.append(fn)
def emit(self, data):
for fn in self._subscribers:
fn(data)
# Composition over inheritance
class Container:
def __init__(self, parts: list):
self.parts = parts # HAS-A, not IS-A
def do_thing(self):
for part in self.parts:
part.do_thing()
Exercises
Exercise 1 — Direct application
Convert the class-based ConversionStrategy example in this post back into a plain dictionary of functions, and confirm it produces identical results with meaningfully less code — the exact comparison this post walked through, done by hand.
Exercise 2 — Slight variation
Add a fourth observer to the ConversionEvent example — one that only prints a message when the conversion type is currency-related (referencing Post #10’s currency feature) — without modifying any of the three existing observers.
Exercise 3 — Real-world combination
Using the Factory pattern, write a function create_bank_account(account_type: str, initial_balance: float) that returns either a regular BankAccount (Post #6/#7) or a new SavingsAccount subclass you define, which adds a interest_rate and an apply_interest() method — routed through the factory based on the account_type string.
Exercise 4 — Open-ended challenge
Take the MultiChannelNotifier composition example and extend it so that adding a new notification channel (a hypothetical SlackNotifier, for instance) requires writing exactly one new small class, with zero changes to MultiChannelNotifier itself — then explain in your own words why this would have been meaningfully harder to achieve with inheritance instead of composition.
FAQ
Q: Do I need to memorize the full “Gang of Four” catalog of 23 design patterns? A: No — most working Python developers use a handful of patterns regularly (the three covered in this post are among the most common) and recognize others by name when they encounter them, without having every pattern’s exact textbook definition memorized. Understanding the underlying problems patterns solve matters far more than reciting their names.
Q: How do I know if I’m over-engineering by using a pattern? A: A useful check: could you explain, concretely, what specific future flexibility or clarity the pattern buys you, beyond “it’s the proper way to do it”? If the honest answer is “not really, for this specific problem,” the simpler alternative is very likely the better choice — exactly the calculation this post walked through for the Strategy pattern’s dictionary-versus-class comparison.
Q: Is it wrong to use the traditional class-based Strategy pattern in Python at all? A: Not wrong — genuinely appropriate when each strategy needs to carry meaningful internal state or configuration of its own, beyond what a single function signature captures. The point of this post is that Python’s first-class functions mean the class-based version is a choice made deliberately for a real reason, not the only correct way to achieve runtime behavior selection.
Q: Are there other common design patterns worth knowing beyond these three?
A: Yes — Singleton, Decorator (a genuine pattern separate from Post #14’s Python-specific @decorator syntax, though conceptually related), Adapter, and Builder are all reasonably common. The three covered in this post were chosen specifically because they connect directly to code already built across this series, making their value concrete rather than abstract.
Summary and Next Steps
You now recognize that Post #5’s CONVERSIONS dictionary was the Strategy pattern all along, understand when the traditional class-based version of Strategy genuinely earns its extra structure versus when a plain dictionary is the more idiomatic Python choice, can apply the Factory pattern to centralize object creation and the Observer pattern to decouple an event from its reactions, and have the composition-over-inheritance principle from Post #6 backed by a concrete example showing exactly why it matters in practice.
Your next step: Complete Exercise 4 — extending MultiChannelNotifier with a new channel and articulating why composition made it easy — because putting the “why” into your own words, after building the concrete example, is what separates recognizing a pattern’s name from genuinely understanding the problem it solves.
The next post moves from how code is structured to how it is actually shipped: packaging the unit converter properly, containerizing it with Docker, and the configuration and logging practices real production Python applications rely on.
Code tested with Python 3.13. Last updated: June 2026.



