Design Patterns in Python
📖 Concept
Design patterns are reusable solutions to commonly occurring problems in software design. In Python, many classic GoF (Gang of Four) patterns are simplified or even unnecessary thanks to the language's dynamic nature — first-class functions, duck typing, and built-in decorators replace verbose class hierarchies.
Categories of design patterns:
| Category | Purpose | Key Patterns |
|---|---|---|
| Creational | Object creation mechanisms | Singleton, Factory, Builder, Prototype |
| Structural | Object composition & relationships | Adapter, Decorator, Facade, Proxy |
| Behavioral | Communication between objects | Observer, Strategy, Command, Iterator |
Pythonic pattern simplifications:
- The Strategy pattern is often replaced by passing functions directly (first-class functions)
- The Iterator pattern is built into Python via
__iter__/__next__and generators - The Decorator pattern maps naturally to Python's
@decoratorsyntax - The Observer pattern can leverage
__set_name__descriptors or signals libraries
Singleton pattern restricts a class to a single instance. In Python, you can use a module-level instance (modules are singletons by nature), a metaclass, or __new__ override. However, singletons are often considered an anti-pattern — prefer dependency injection for testability.
Factory pattern delegates object creation to a factory function or class, decoupling the client from concrete implementations. Python's dynamic typing and duck typing make factories lightweight — a simple function returning different objects based on input is often sufficient.
Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified. Python implementations often use callback lists, weakref to avoid memory leaks, or third-party libraries like blinker.
When to use patterns: Apply patterns when you recognize the problem they solve, not preemptively. Over-engineering with patterns is as harmful as ignoring them entirely. Python's motto — "simple is better than complex" — applies.
💻 Code Example
1# ============================================================2# Design Patterns in Python — Practical Implementations3# ============================================================45from abc import ABC, abstractmethod6from typing import Any, Callable7import weakref8910# --- Singleton Pattern (using __new__) ---11class DatabaseConnection:12 """Only one database connection instance exists."""13 _instance = None1415 def __new__(cls, *args, **kwargs):16 if cls._instance is None:17 cls._instance = super().__new__(cls)18 cls._instance._initialized = False19 return cls._instance2021 def __init__(self, connection_string: str = "sqlite:///default.db"):22 if self._initialized:23 return24 self.connection_string = connection_string25 self._initialized = True26 print(f"Connected to {connection_string}")272829# Both variables point to the same instance30db1 = DatabaseConnection("postgres://localhost/mydb")31db2 = DatabaseConnection("postgres://other") # Ignored — already initialized32assert db1 is db2 # True333435# --- Factory Pattern ---36class Serializer(ABC):37 @abstractmethod38 def serialize(self, data: dict) -> str: ...3940class JSONSerializer(Serializer):41 def serialize(self, data: dict) -> str:42 import json43 return json.dumps(data, indent=2)4445class XMLSerializer(Serializer):46 def serialize(self, data: dict) -> str:47 items = "".join(f" <{k}>{v}</{k}>\n" for k, v in data.items())48 return f"<root>\n{items}</root>"4950class CSVSerializer(Serializer):51 def serialize(self, data: dict) -> str:52 header = ",".join(data.keys())53 values = ",".join(str(v) for v in data.values())54 return f"{header}\n{values}"5556def create_serializer(format_type: str) -> Serializer:57 """Factory function — decouples creation from usage."""58 serializers = {59 "json": JSONSerializer,60 "xml": XMLSerializer,61 "csv": CSVSerializer,62 }63 cls = serializers.get(format_type)64 if cls is None:65 raise ValueError(f"Unknown format: {format_type}")66 return cls()6768# Usage — client code doesn't know concrete classes69data = {"name": "Alice", "age": 30, "role": "Engineer"}70for fmt in ("json", "xml", "csv"):71 serializer = create_serializer(fmt)72 print(f"--- {fmt.upper()} ---")73 print(serializer.serialize(data))747576# --- Observer Pattern ---77class EventEmitter:78 """Lightweight observer using weak references."""7980 def __init__(self):81 self._listeners: dict[str, list] = {}8283 def on(self, event: str, callback: Callable) -> None:84 self._listeners.setdefault(event, []).append(callback)8586 def emit(self, event: str, *args, **kwargs) -> None:87 for callback in self._listeners.get(event, []):88 callback(*args, **kwargs)8990 def off(self, event: str, callback: Callable) -> None:91 listeners = self._listeners.get(event, [])92 self._listeners[event] = [cb for cb in listeners if cb != callback]9394# Usage95emitter = EventEmitter()9697def on_user_created(user: dict):98 print(f"Welcome email sent to {user['email']}")99100def on_user_created_log(user: dict):101 print(f"[LOG] User created: {user['name']}")102103emitter.on("user_created", on_user_created)104emitter.on("user_created", on_user_created_log)105emitter.emit("user_created", {"name": "Bob", "email": "bob@example.com"})106107108# --- Strategy Pattern (Pythonic — just pass functions) ---109def sort_users(users: list[dict], strategy: Callable) -> list[dict]:110 """Strategy pattern via first-class functions."""111 return sorted(users, key=strategy)112113users = [114 {"name": "Charlie", "age": 25},115 {"name": "Alice", "age": 30},116 {"name": "Bob", "age": 22},117]118119by_name = sort_users(users, strategy=lambda u: u["name"])120by_age = sort_users(users, strategy=lambda u: u["age"])121print("By name:", [u["name"] for u in by_name])122print("By age:", [u["name"] for u in by_age])123124125# --- Decorator Pattern (class-based, not @decorator syntax) ---126class NotificationService:127 def send(self, message: str) -> str:128 return f"Sending: {message}"129130class LoggingDecorator:131 """Wraps a service, adding logging behavior."""132 def __init__(self, wrapped: NotificationService):133 self._wrapped = wrapped134135 def send(self, message: str) -> str:136 print(f"[LOG] About to send: {message}")137 result = self._wrapped.send(message)138 print(f"[LOG] Sent successfully")139 return result140141service = LoggingDecorator(NotificationService())142service.send("Hello, World!")
🏋️ Practice Exercise
Exercises:
Implement a Registry pattern — create a class
PluginRegistrywhere plugins register themselves with a decorator (@PluginRegistry.register). Write 3 sample plugins and retrieve them by name.Build a Builder pattern for constructing an
HTTPRequestobject with method chaining:Request().method("POST").url("/api/users").header("Content-Type", "application/json").body(data).build().Implement the Observer pattern for a stock price tracker. When a stock price changes, notify all registered observers (e.g.,
EmailAlert,DashboardUpdate,LogWriter). Useweakrefto avoid memory leaks.Refactor the Strategy pattern example to support a discount calculation system:
PercentageDiscount,FixedDiscount,BuyOneGetOneFree. Implement both a class-based and a function-based approach, then compare readability.Create a Proxy pattern that wraps a
DatabaseServiceclass, adding caching (return cached results for repeated queries) and access control (check user permissions before executing queries).
⚠️ Common Mistakes
Over-engineering with patterns — forcing a Strategy class hierarchy when a simple function parameter would suffice. Python's first-class functions eliminate the need for many classic patterns.
Implementing Singleton with global variables instead of controlling instantiation. Module-level instances are the most Pythonic singleton approach.
Using the Observer pattern without weak references, causing memory leaks when observers are deleted but still referenced by the subject.
Applying GoF patterns verbatim from Java/C++ without adapting to Python idioms. Python's duck typing, decorators, and closures replace many structural patterns.
Not considering testability — Singletons and global state make unit testing difficult. Prefer dependency injection over Singletons.
💼 Interview Questions
🎤 Mock Interview
Practice a live interview for Design Patterns in Python