Python Design Patterns
Implement classic design patterns in Python: singleton, factory, observer, strategy, and decorator with idiomatic Pythonic approaches.
Singleton Pattern
Ensures only one instance of a class exists.
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, value=None):
# Guard against re-initialization
if not hasattr(self, "_initialized"):
self.value = value
self._initialized = True
s1 = Singleton("first")
s2 = Singleton("second")
s1 is s2 # True
s1.value # "first" — init only ran once
Thread-Safe Singleton
import threading
class ThreadSafeSingleton:
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None: # double-checked locking
cls._instance = super().__new__(cls)
return cls._instance
Pythonic Alternative: Module-Level Instance
# config.py
class Config:
def __init__(self):
self.debug = False
self.database_url = "sqlite:///app.db"
# Single instance created once at import time
config = Config()
# Other modules
from config import config
config.debug = True
Factory Pattern
Creates objects without specifying the exact class.
from __future__ import annotations
from abc import ABC, abstractmethod
class Notification(ABC):
@abstractmethod
def send(self, message: str) -> None:
pass
class EmailNotification(Notification):
def __init__(self, address: str):
self.address = address
def send(self, message: str) -> None:
print(f"Email to {self.address}: {message}")
class SMSNotification(Notification):
def __init__(self, phone: str):
self.phone = phone
def send(self, message: str) -> None:
print(f"SMS to {self.phone}: {message}")
class PushNotification(Notification):
def send(self, message: str) -> None:
print(f"Push: {message}")
# Factory function — simpler than a factory class in Python
def create_notification(type_: str, **kwargs) -> Notification:
registry = {
"email": EmailNotification,
"sms": SMSNotification,
"push": PushNotification,
}
if type_ not in registry:
raise ValueError(f"Unknown notification type: {type_}")
return registry[type_](**kwargs)
notif = create_notification("email", address="alice@example.com")
notif.send("Your order shipped!")
Observer Pattern
Lets objects subscribe to events fired by another object.
from typing import Callable, Any
class EventEmitter:
def __init__(self):
self._listeners: dict[str, list[Callable]] = {}
def on(self, event: str, listener: Callable) -> None:
self._listeners.setdefault(event, []).append(listener)
def off(self, event: str, listener: Callable) -> None:
self._listeners.get(event, []).remove(listener)
def emit(self, event: str, *args: Any, **kwargs: Any) -> None:
for listener in self._listeners.get(event, []):
listener(*args, **kwargs)
# Usage
class OrderService(EventEmitter):
def place_order(self, order: dict) -> None:
# ... process order ...
self.emit("order_placed", order)
self.emit("inventory_updated", order["items"])
service = OrderService()
service.on("order_placed", lambda o: print(f"Sending confirmation for {o['id']}"))
service.on("order_placed", lambda o: print(f"Charging card for ${o['total']}"))
service.place_order({"id": 42, "total": 99.99, "items": ["book"]})
Strategy Pattern
Defines a family of algorithms and makes them interchangeable.
from typing import Protocol
import heapq
class SortStrategy(Protocol):
def sort(self, data: list) -> list:
...
class QuickSort:
def sort(self, data: list) -> list:
if len(data) <= 1:
return data
pivot = data[len(data) // 2]
left = [x for x in data if x < pivot]
middle = [x for x in data if x == pivot]
right = [x for x in data if x > pivot]
return self.sort(left) + middle + self.sort(right)
class MergeSort:
def sort(self, data: list) -> list:
if len(data) <= 1:
return data
mid = len(data) // 2
left = self.sort(data[:mid])
right = self.sort(data[mid:])
return self._merge(left, right)
def _merge(self, left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i]); i += 1
else:
result.append(right[j]); j += 1
return result + left[i:] + right[j:]
class Sorter:
def __init__(self, strategy: SortStrategy):
self.strategy = strategy
def sort(self, data: list) -> list:
return self.strategy.sort(data)
sorter = Sorter(QuickSort())
sorter.sort([5, 3, 1, 4, 2]) # [1, 2, 3, 4, 5]
sorter.strategy = MergeSort() # swap at runtime
sorter.sort([5, 3, 1, 4, 2])
Decorator Pattern
Wraps an object to add behavior without subclassing.
from abc import ABC, abstractmethod
class DataSource(ABC):
@abstractmethod
def write(self, data: str) -> None: ...
@abstractmethod
def read(self) -> str: ...
class FileDataSource(DataSource):
def __init__(self, path: str):
self.path = path
def write(self, data: str) -> None:
with open(self.path, "w") as f:
f.write(data)
def read(self) -> str:
with open(self.path) as f:
return f.read()
class EncryptionDecorator(DataSource):
def __init__(self, wrapped: DataSource):
self._wrapped = wrapped
def write(self, data: str) -> None:
self._wrapped.write(self._encrypt(data))
def read(self) -> str:
return self._decrypt(self._wrapped.read())
def _encrypt(self, data: str) -> str:
return data[::-1] # trivial example
def _decrypt(self, data: str) -> str:
return data[::-1]
class CompressionDecorator(DataSource):
def __init__(self, wrapped: DataSource):
self._wrapped = wrapped
def write(self, data: str) -> None:
import zlib, base64
compressed = base64.b64encode(zlib.compress(data.encode())).decode()
self._wrapped.write(compressed)
def read(self) -> str:
import zlib, base64
raw = self._wrapped.read()
return zlib.decompress(base64.b64decode(raw)).decode()
# Stack decorators
source = CompressionDecorator(EncryptionDecorator(FileDataSource("data.bin")))
source.write("Hello, World!")
source.read() # "Hello, World!"
Command Pattern
Encapsulates operations as objects — enables undo/redo and queuing.
from abc import ABC, abstractmethod
from collections import deque
class Command(ABC):
@abstractmethod
def execute(self) -> None: ...
@abstractmethod
def undo(self) -> None: ...
class TextEditor:
def __init__(self):
self.text = ""
self._history: deque[Command] = deque()
def execute(self, cmd: Command) -> None:
cmd.execute()
self._history.append(cmd)
def undo(self) -> None:
if self._history:
self._history.pop().undo()
class InsertText(Command):
def __init__(self, editor: TextEditor, text: str):
self.editor = editor
self.text = text
def execute(self) -> None:
self.editor.text += self.text
def undo(self) -> None:
self.editor.text = self.editor.text[:-len(self.text)]
editor = TextEditor()
editor.execute(InsertText(editor, "Hello"))
editor.execute(InsertText(editor, ", World"))
print(editor.text) # "Hello, World"
editor.undo()
print(editor.text) # "Hello" Frequently Asked Questions
Are design patterns necessary in Python?
Many Gang of Four patterns are simpler in Python due to first-class functions, dynamic typing, and duck typing. Some patterns (like Iterator or Strategy) are built into the language. Learn patterns as solutions to problems, not as recipes to apply everywhere.
Is the Singleton pattern bad?
Singletons make testing hard because they carry global state between tests. Prefer dependency injection — pass the shared object explicitly instead of using a global singleton.
What's the difference between the decorator pattern and Python decorators?
The decorator design pattern wraps an object to add behavior (same interface, extra functionality). Python decorators are a language feature that wraps functions or classes — they can implement the decorator pattern, but they're more general.