Why Design Patterns?
Design patterns are proven solutions to common software design problems. They're not code you copy-paste — they're templates for thinking about how to structure your code. Python's dynamic nature means many patterns are simpler than in Java or C++, and some are built right into the language.
Patterns are tools, not rules
Don't force patterns where they don't fit. A simple function is better than a over-engineered pattern. Use patterns when they solve a real problem in your code, not because they're "best practice."
Singleton — One Instance Only
A Singleton ensures only one instance of a class exists. In Python, this is often done with module-level variables (which are naturally singletons) or with __new__:
class DatabaseConnection:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self, host="localhost"):
if self._initialized:
return
self.host = host
self.connected = True
self._initialized = True
print(f"Connected to {host}")
# Both variables point to the SAME instance
db1 = DatabaseConnection("prod-server")
db2 = DatabaseConnection("other-server") # Ignored — already initialized
print(f"Same instance? {db1 is db2}")
print(f"Host: {db2.host}")
Connected to prod-server Same instance? True Host: prod-server
Pythonic alternative: Just use a module variable. Python modules are singletons by default — importing the same module twice gives you the same object.
Factory — Create Without Specifying Class
A Factory creates objects without the caller needing to know the exact class. This is useful when the type depends on input data or configuration:
from abc import ABC, abstractmethod
class Serializer(ABC):
@abstractmethod
def serialize(self, data: dict) -> str: ...
class JSONSerializer(Serializer):
def serialize(self, data):
import json
return json.dumps(data, indent=2)
class CSVSerializer(Serializer):
def serialize(self, data):
headers = ",".join(data.keys())
values = ",".join(str(v) for v in data.values())
return f"{headers}\n{values}"
class XMLSerializer(Serializer):
def serialize(self, data):
items = "".join(f"<{k}>{v}{k}>" for k, v in data.items())
return f"{items} "
def get_serializer(format: str) -> Serializer:
"""Factory function — returns the right serializer."""
serializers = {
"json": JSONSerializer,
"csv": CSVSerializer,
"xml": XMLSerializer,
}
if format not in serializers:
raise ValueError(f"Unknown format: {format}")
return serializers[format]()
# Usage — caller doesn't know which class is used
data = {"name": "Alice", "age": 30, "city": "NYC"}
for fmt in ["json", "csv", "xml"]:
s = get_serializer(fmt)
print(f"--- {fmt.upper()} ---")
print(s.serialize(data))
--- JSON ---
{
"name": "Alice",
"age": 30,
"city": "NYC"
}
--- CSV ---
name,age,city
Alice,30,NYC
--- XML ---
Alice 30 NYC Observer — Event System
The Observer pattern lets objects subscribe to events and get notified when something happens. It decouples the event source from the handlers:
class EventEmitter:
def __init__(self):
self._listeners = {}
def on(self, event: str, callback):
"""Subscribe to an event."""
self._listeners.setdefault(event, []).append(callback)
def emit(self, event: str, *args, **kwargs):
"""Notify all subscribers of an event."""
for callback in self._listeners.get(event, []):
callback(*args, **kwargs)
# Usage
emitter = EventEmitter()
# Subscribe handlers
emitter.on("user_created", lambda name: print(f" Welcome email sent to {name}"))
emitter.on("user_created", lambda name: print(f" Analytics: new user {name}"))
emitter.on("user_created", lambda name: print(f" Slack notification: {name} joined"))
# Emit event — all handlers run
print("Creating user...")
emitter.emit("user_created", "Alice")
Creating user... Welcome email sent to Alice Analytics: new user Alice Slack notification: Alice joined
Strategy — Swappable Algorithms
Strategy lets you swap algorithms at runtime. In Python, functions are first-class objects, so this pattern is beautifully simple:
from typing import Callable
# Different pricing strategies
def standard_price(base: float) -> float:
return base
def premium_discount(base: float) -> float:
return base * 0.8 # 20% off
def bulk_discount(base: float) -> float:
return base * 0.6 # 40% off
class ShoppingCart:
def __init__(self, pricing: Callable[[float], float] = standard_price):
self.items: list[tuple[str, float]] = []
self.pricing = pricing # Strategy!
def add(self, name: str, price: float):
self.items.append((name, price))
def total(self) -> float:
return sum(self.pricing(price) for _, price in self.items)
# Same cart, different pricing
cart = ShoppingCart(pricing=premium_discount)
cart.add("Laptop", 1000)
cart.add("Mouse", 50)
print(f"Premium total: ${cart.total():.2f}")
cart.pricing = bulk_discount # Swap strategy at runtime!
print(f"Bulk total: ${cart.total():.2f}")
Premium total: $840.00 Bulk total: $630.00
Decorator Pattern (Not @decorator)
The GoF Decorator pattern wraps an object to add behavior without modifying the original. Don't confuse this with Python's @decorator syntax (which is related but different):
class Coffee:
def cost(self) -> float:
return 5.0
def description(self) -> str:
return "Coffee"
class MilkDecorator:
def __init__(self, coffee):
self._coffee = coffee
def cost(self) -> float:
return self._coffee.cost() + 2.0
def description(self) -> str:
return self._coffee.description() + " + Milk"
class SugarDecorator:
def __init__(self, coffee):
self._coffee = coffee
def cost(self) -> float:
return self._coffee.cost() + 0.5
def description(self) -> str:
return self._coffee.description() + " + Sugar"
# Build up a drink with decorators
drink = Coffee()
drink = MilkDecorator(drink)
drink = SugarDecorator(drink)
drink = SugarDecorator(drink) # Extra sugar!
print(f"{drink.description()}: ${drink.cost():.2f}")
Coffee + Milk + Sugar + Sugar: $8.00
Pattern Summary
| Pattern | Problem | Pythonic Approach |
|---|---|---|
| Singleton | One instance only | Module variable or __new__ |
| Factory | Create without specifying class | Function with dict of classes |
| Observer | React to events | Callback lists or signals library |
| Strategy | Swappable algorithms | Pass functions as arguments |
| Decorator | Add behavior to objects | Wrapper classes or @decorator |
| Iterator | Traverse collections | Built-in with __iter__/__next__ |
| Context Manager | Resource management | Built-in with with/__enter__/__exit__ |
🔍 Deep Dive: Python Has Patterns Built In
Many GoF patterns are unnecessary in Python because the language has them built in. Iterator = for loops and generators. Strategy = first-class functions. Template Method = default arguments and inheritance. Command = callable objects. Null Object = None with or operator. Before implementing a pattern, check if Python already provides a simpler way.