Design patterns were named and cataloged in a 1994 book written against Java- and C++-style languages. Python's first-class functions, duck typing, and built-in decorator syntax mean several of them collapse into something much simpler here. This lesson covers four patterns you'll actually see in real Python codebases, in their genuinely idiomatic form.
1Strategy and Factory: Patterns That Get Simpler in Python
The Strategy Pattern's textbook form (from the 1994 Gang of Four book, written against Java and C++) defines a Strategy interface, then a separate concrete class per algorithm variant, because those languages have no way to pass a bare function around as a first-class value ā you need an object wrapping it just to hand it to another piece of code. Python has no such restriction: functions are ordinary values, storable in variables, passable as arguments, callable directly. checkout(100, apply_percent) *is* the Strategy Pattern, fully realized, with zero interface boilerplate ā swapping apply_percent for apply_fixed at the call site is exactly 'swap the algorithm at runtime', the pattern's entire purpose.
The Factory Pattern translates more directly, because its core idea ā centralizing object-creation logic so callers don't need to know which concrete class to instantiate ā is still valuable in Python; a create_parser("json") function hides JsonParser behind a stable, simple interface, letting the concrete class set grow (adding create_parser("yaml")) without any caller needing to import or even know about the new class directly. Where Python simplifies this pattern is in the implementation: a plain function (often using match/case, as covered earlier in this module) usually replaces the more ceremonial 'AbstractFactory + ConcreteFactory classes' structure Java-style implementations often reach for.
The throughline: both patterns exist to solve real design problems (swappable behavior, centralized creation), and those problems are just as real in Python ā but Python's language features (first-class functions, match/case) frequently let you solve them with dramatically less structural ceremony than the pattern's original textbook form implies is necessary.
def apply_percent(total): return total * 0.9
def apply_fixed(total): return max(0, total - 10)
def checkout(total: float, strategy) -> float:
return strategy(total)
checkout(100, apply_percent) # 90.0
checkout(100, apply_fixed) # 90.0 -- just pass a different functionThe entire Strategy Pattern ā one function parameter
2Observer: Decoupling Publishers From Subscribers
The Observer Pattern solves a specific coupling problem: a piece of code that produces events (a publisher) shouldn't need to know, at write time, everything that might eventually want to react to those events (subscribers) ā new subscribers should be addable without ever modifying the publisher. EventBus.publish() only knows it can call each registered callback with an event; it has no import, no reference, no knowledge whatsoever of what bus.subscribe(lambda e: print(f"Logged: {e}"))'s specific lambda actually does.
This decoupling is what makes the pattern valuable in real systems: a logging subscriber, an analytics subscriber, and a notification subscriber can all register independently, be added or removed at runtime, and none of them ā nor EventBus itself ā needs to know the others exist. Compare this to a publisher that directly calls log_event(e); send_analytics(e); notify_user(e) inline: every new reaction requires editing the publisher directly, exactly the kind of modification-for-extension OCP (from the SOLID lesson) warns against.
Python's Observer implementations often use plain callables (functions, lambdas, or bound methods) as subscribers, exactly as shown here, rather than requiring subscribers to implement a formal Observer interface class ā another case of duck typing collapsing ceremony that a statically-typed language's version of the pattern requires. EventBus doesn't care *what* a subscriber is, only that calling it with one argument works.
def create_parser(file_type: str):
match file_type:
case "csv": return CsvParser()
case "json": return JsonParser()
case _: raise ValueError(f"Unknown type: {file_type}")
parser = create_parser("json") # caller doesn't need to know JsonParser existsEventBus never references any specific subscriber's logic
3Decorator: The Pattern Python Built Into Its Syntax
The Decorator Pattern's purpose ā attach additional responsibilities to an object dynamically, without altering its own source code or affecting other instances of the same class ā describes, almost word for word, exactly what Python's @decorator syntax (covered in depth in the Decorators lesson) does for functions. with_logging wraps process_order, adding a logging responsibility, without a single line inside process_order itself changing ā this is the Decorator Pattern, not merely 'inspired by' it or 'similar to' it.
This is a genuinely useful realization for reading design-patterns literature written for other languages: where a Java codebase might implement the Decorator Pattern as a class implementing the same interface as the object it wraps, holding a reference to that wrapped object, and delegating most calls while adding behavior around specific ones ā Python gets the same structural benefit from a function wrapping a function, made ergonomic enough that the language gave it dedicated syntax.
Recognizing this pattern-to-language-feature mapping generalizes: understanding the *problem* each classic pattern solves (swappable behavior, centralized creation, decoupled event handling, dynamically added responsibilities) is more durable knowledge than memorizing each pattern's textbook Java implementation, precisely because different languages solve the same underlying problems with different amounts of ceremony ā and Python, deliberately, tends to need less.
class EventBus:
def __init__(self):
self._subscribers: list = []
def subscribe(self, callback) -> None:
self._subscribers.append(callback)
def publish(self, event) -> None:
for callback in self._subscribers:
callback(event)
bus = EventBus()
bus.subscribe(lambda e: print(f"Logged: {e}"))
bus.publish("user_signed_up")The Decorator Pattern, expressed as native language syntax
4Step-by-Step Breakdown
The 'Strategy Pattern' in Java is a whole file of interfaces and classes. In Python, it's sometimes just... a function. Let's see why.
Strategy Pattern: swap an algorithm at runtime. In Python, a first-class function often replaces an entire 'Strategy interface + concrete classes' hierarchy.
Checkpoint: Why can Python's Strategy Pattern often skip defining a Strategy interface class entirely?
- āFunctions are first-class objects in Python, so a plain function already satisfies "something callable with this signature"
- āPython does not support the Strategy Pattern at all
Factory Pattern: centralize object creation logic so callers don't need to know which concrete class to instantiate.
Observer Pattern: let objects subscribe to events without the publisher knowing anything about who's listening.
Checkpoint: In the Observer Pattern example, does EventBus need to know anything about what its subscribers do?
- āNo ā EventBus only knows it can call each subscriber with an event; the subscriber logic is fully decoupled
- āYes ā EventBus must import and know about each subscriber's specific class
Decorator Pattern: add responsibilities to an object dynamically. Python's @decorator syntax IS a built-in implementation of this exact pattern for functions.
That completes the design-thinking layer of this module ā Abstract Base Classes and Protocols next give you the formal tools these patterns are often expressed through.
Swap a Real Strategy at Runtime. Finish checkout(): a plain function already satisfies a Strategy interface in Python.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Best Practices
Learn the problem each pattern solves, not just its textbook Java implementation
Strategy, Factory, Observer, and Decorator all solve real, recurring design problems ā but their idiomatic Python form (a function, a match statement, a callable list, @decorator) often looks nothing like the class-heavy structure most pattern references show first.
Reach for a pattern because a specific problem is present, not to look sophisticated
A three-line function doesn't need a Strategy interface wrapped around it 'for extensibility' if there's only ever going to be one implementation ā apply a pattern once the actual problem (needing to swap behavior, decouple publishers/subscribers) genuinely shows up.
Frequent Bugs
Porting a design pattern's Java/C# textbook structure literally into Python (a full ConcreteStrategyA/ConcreteStrategyB class hierarchy for what could be two plain functions), adding unnecessary ceremony.
Before implementing a pattern, ask what Python language feature (first-class functions, match/case, @decorator, duck typing) might already solve the same underlying problem more simply than the pattern's original class-based form.
Real-World Examples
A Pluggable Notification System Using Strategy and Observer Together
A system needs to send notifications through swappable channels (email, SMS, push) and needs other parts of the codebase to react to "notification sent" events without coupling to the notification logic itself.
def send_email(user, message): ...
def send_sms(user, message): ...
class NotificationService:
def __init__(self, channel, event_bus):
self.channel = channel # Strategy
self.event_bus = event_bus # Observer publisher
def notify(self, user, message):
self.channel(user, message)
self.event_bus.publish(f"notified:{user}")
service = NotificationService(send_email, EventBus())