Design Patterns in Python

0/3 in this phase0/54 across the roadmap

📖 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 @decorator syntax
  • 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

codeTap to expand ⛶
1# ============================================================
2# Design Patterns in PythonPractical Implementations
3# ============================================================
4
5from abc import ABC, abstractmethod
6from typing import Any, Callable
7import weakref
8
9
10# --- Singleton Pattern (using __new__) ---
11class DatabaseConnection:
12 """Only one database connection instance exists."""
13 _instance = None
14
15 def __new__(cls, *args, **kwargs):
16 if cls._instance is None:
17 cls._instance = super().__new__(cls)
18 cls._instance._initialized = False
19 return cls._instance
20
21 def __init__(self, connection_string: str = "sqlite:///default.db"):
22 if self._initialized:
23 return
24 self.connection_string = connection_string
25 self._initialized = True
26 print(f"Connected to {connection_string}")
27
28
29# Both variables point to the same instance
30db1 = DatabaseConnection("postgres://localhost/mydb")
31db2 = DatabaseConnection("postgres://other") # Ignored — already initialized
32assert db1 is db2 # True
33
34
35# --- Factory Pattern ---
36class Serializer(ABC):
37 @abstractmethod
38 def serialize(self, data: dict) -> str: ...
39
40class JSONSerializer(Serializer):
41 def serialize(self, data: dict) -> str:
42 import json
43 return json.dumps(data, indent=2)
44
45class 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>"
49
50class 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}"
55
56def 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()
67
68# Usage — client code doesn't know concrete classes
69data = {"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))
74
75
76# --- Observer Pattern ---
77class EventEmitter:
78 """Lightweight observer using weak references."""
79
80 def __init__(self):
81 self._listeners: dict[str, list] = {}
82
83 def on(self, event: str, callback: Callable) -> None:
84 self._listeners.setdefault(event, []).append(callback)
85
86 def emit(self, event: str, *args, **kwargs) -> None:
87 for callback in self._listeners.get(event, []):
88 callback(*args, **kwargs)
89
90 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]
93
94# Usage
95emitter = EventEmitter()
96
97def on_user_created(user: dict):
98 print(f"Welcome email sent to {user['email']}")
99
100def on_user_created_log(user: dict):
101 print(f"[LOG] User created: {user['name']}")
102
103emitter.on("user_created", on_user_created)
104emitter.on("user_created", on_user_created_log)
105emitter.emit("user_created", {"name": "Bob", "email": "bob@example.com"})
106
107
108# --- 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)
112
113users = [
114 {"name": "Charlie", "age": 25},
115 {"name": "Alice", "age": 30},
116 {"name": "Bob", "age": 22},
117]
118
119by_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])
123
124
125# --- Decorator Pattern (class-based, not @decorator syntax) ---
126class NotificationService:
127 def send(self, message: str) -> str:
128 return f"Sending: {message}"
129
130class LoggingDecorator:
131 """Wraps a service, adding logging behavior."""
132 def __init__(self, wrapped: NotificationService):
133 self._wrapped = wrapped
134
135 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 result
140
141service = LoggingDecorator(NotificationService())
142service.send("Hello, World!")

🏋️ Practice Exercise

Exercises:

  1. Implement a Registry pattern — create a class PluginRegistry where plugins register themselves with a decorator (@PluginRegistry.register). Write 3 sample plugins and retrieve them by name.

  2. Build a Builder pattern for constructing an HTTPRequest object with method chaining: Request().method("POST").url("/api/users").header("Content-Type", "application/json").body(data).build().

  3. Implement the Observer pattern for a stock price tracker. When a stock price changes, notify all registered observers (e.g., EmailAlert, DashboardUpdate, LogWriter). Use weakref to avoid memory leaks.

  4. 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.

  5. Create a Proxy pattern that wraps a DatabaseService class, 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