Design patterns have a reputation for being over-applied. Someone reads the Gang of Four book, discovers Singleton and Factory, and starts reaching for them everywhere — including places where a plain function would be cleaner and easier to test.
This isn't that kind of post.
The 10 patterns below are the ones that genuinely appear in Django, Flask, FastAPI, and standard library Python. For each one: what it does, where you already encounter it, working code, and an honest take on when not to use it.
Why Patterns Still Matter in Python
Python lets you implement the same thing six different ways. That's a feature until you're on a team, then it's a coordination problem.
Patterns give common names to recurring structural decisions. "We're using Observer here" tells a developer the intent without them reading every line. Strategy and Factory make logic independently testable. Facade and Adapter let you swap underlying systems without touching calling code.
The caveat: patterns add abstraction, abstraction has a cost. Use them when they solve a real problem. A simple class is always better than a pattern that adds complexity without adding clarity.
Creational Patterns
1. Singleton
One instance, global access point. Python's logging module is a Singleton. Django's database connection manager too.
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super().__new__(cls, *args, **kwargs)
return cls._instance
instance1 = Singleton()
instance2 = Singleton()
print(instance1 is instance2) # True
💡 Honest note: For most Python use cases, a module-level variable is a simpler Singleton. Use class-based Singleton when you need lazy initialization or subclassing.
Skip it when: it introduces hidden global state that makes unit tests depend on each other.
2. Factory
Object type decided at runtime. Flask's create_app() is Factory. Django's form field creation uses it too.
class Dog:
def speak(self): return "Woof!"
class Cat:
def speak(self): return "Meow!"
class AnimalFactory:
@staticmethod
def create_animal(animal_type: str):
animals = {"dog": Dog, "cat": Cat}
animal_class = animals.get(animal_type.lower())
if not animal_class:
raise ValueError(f"Unknown animal type: {animal_type}")
return animal_class()
print(AnimalFactory.create_animal("dog").speak()) # Woof!
Skip it when: you're only ever creating one type of object. Factory adds an abstraction layer — that cost is only worth it when the flexibility actually gets used.
3. Abstract Factory
Factory of factories. Creates families of related objects. Classic example: UI toolkits that generate platform-specific widgets.
from abc import ABC, abstractmethod
class Button(ABC):
@abstractmethod
def render(self): pass
class WindowsButton(Button):
def render(self): return "Rendering Windows button"
class MacButton(Button):
def render(self): return "Rendering Mac button"
class GUIFactory(ABC):
@abstractmethod
def create_button(self) -> Button: pass
class WindowsFactory(GUIFactory):
def create_button(self) -> Button: return WindowsButton()
class MacFactory(GUIFactory):
def create_button(self) -> Button: return MacButton()
factory = WindowsFactory()
print(factory.create_button().render()) # Rendering Windows button
Skip it when: there's only one product family. Abstract Factory is more complex than plain Factory — use it only when the complexity is genuinely needed.
4. Builder
Complex object construction with many optional params. SQL query builders, HTTP request builders, pytest fixtures.
class QueryBuilder:
def __init__(self):
self._table = ""
self._conditions = []
self._limit = None
def from_table(self, table: str):
self._table = table
return self
def where(self, condition: str):
self._conditions.append(condition)
return self
def limit(self, count: int):
self._limit = count
return self
def build(self) -> str:
query = f"SELECT * FROM {self._table}"
if self._conditions:
query += " WHERE " + " AND ".join(self._conditions)
if self._limit:
query += f" LIMIT {self._limit}"
return query
query = (
QueryBuilder()
.from_table("users")
.where("age > 18")
.where("active = true")
.limit(10)
.build()
)
# SELECT * FROM users WHERE age > 18 AND active = true LIMIT 10
Skip it when: object has 2-3 fields. A dataclass or regular constructor is the right call for simple objects.
5. Prototype
Clone instead of construct — useful when initialization is expensive (DB queries, API calls, heavy config).
import copy
class Config:
def __init__(self, settings: dict):
self.settings = settings
def clone(self):
return copy.deepcopy(self)
base_config = Config({"debug": False, "db_host": "localhost", "timeout": 30})
prod_config = base_config.clone()
prod_config.settings["db_host"] = "prod-db.example.com"
print(base_config.settings["db_host"]) # localhost
print(prod_config.settings["db_host"]) # prod-db.example.com
Skip it when: object creation is cheap. copy.deepcopy() has its own cost and can behave unexpectedly with objects containing file handles or DB connections.
Structural Patterns
6. Adapter
Makes incompatible interfaces work together. Used heavily when integrating third-party libraries or wrapping legacy code.
class EuropeanSocket:
def voltage(self): return 230
def live(self): return "L"
class EuropeanToUSAAdapter:
def __init__(self, socket: EuropeanSocket):
self._socket = socket
def voltage(self): return 120
def neutral(self): return self._socket.live()
adapter = EuropeanToUSAAdapter(EuropeanSocket())
print(adapter.voltage()) # 120
print(adapter.neutral()) # L
Skip it when: you have access to the original source. Adapter adds indirection — fix the interface directly when possible.
7. Decorator
The most Pythonic pattern in the list — @decorator syntax IS the Decorator pattern. @property, @login_required, @app.route — you're already using it.
import functools, time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
print(f"{func.__name__} ran in {time.perf_counter() - start:.4f}s")
return result
return wrapper
def log_call(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args}")
return func(*args, **kwargs)
return wrapper
@timer
@log_call
def fetch_data(user_id: int):
time.sleep(0.1)
return {"user_id": user_id, "name": "Alice"}
fetch_data(42)
# Calling fetch_data with args=(42,)
# fetch_data ran in 0.1003s
⚠️ More than 3-4 decorators on one function is a red flag. Execution order gets confusing and debugging gets painful fast.
8. Facade
Simple interface over a complex subsystem. Django's ORM is a Facade over raw SQL. requests is a Facade over urllib.
class CPU:
def freeze(self): print("CPU: Freeze")
def jump(self, address): print(f"CPU: Jump to {address}")
def execute(self): print("CPU: Execute")
class Memory:
def load(self, address, data): print(f"Memory: Load {data} at {address}")
class HardDrive:
def read(self, sector, size): return f"Data from sector {sector}"
class ComputerFacade:
def __init__(self):
self.cpu = CPU()
self.memory = Memory()
self.hard_drive = HardDrive()
def start(self):
self.cpu.freeze()
data = self.hard_drive.read(sector=0, size=1024)
self.memory.load(address=0, data=data)
self.cpu.jump(address=0)
self.cpu.execute()
ComputerFacade().start()
Skip it when: there's no real complexity to hide. A Facade that just proxies to a single class is noise — and aggressive Facades get bypassed by developers who need the underlying behavior, which is worse than not having them.
Behavioral Patterns
9. Strategy
Swappable algorithms at runtime. Payment methods, sorting implementations, auth backends in Django.
from abc import ABC, abstractmethod
class SortStrategy(ABC):
@abstractmethod
def sort(self, data: list) -> list: pass
class BubbleSort(SortStrategy):
def sort(self, data: list) -> list:
data = data.copy()
n = len(data)
for i in range(n):
for j in range(0, n - i - 1):
if data[j] > data[j + 1]:
data[j], data[j + 1] = data[j + 1], data[j]
return data
class PythonBuiltinSort(SortStrategy):
def sort(self, data: list) -> list: return sorted(data)
class Sorter:
def __init__(self, strategy: SortStrategy):
self._strategy = strategy
def set_strategy(self, strategy: SortStrategy):
self._strategy = strategy
def sort(self, data: list) -> list:
return self._strategy.sort(data)
data = [5, 2, 8, 1, 9]
sorter = Sorter(PythonBuiltinSort())
print(sorter.sort(data)) # [1, 2, 5, 8, 9]
sorter.set_strategy(BubbleSort())
print(sorter.sort(data)) # [1, 2, 5, 8, 9]
Skip it when: there's only one algorithm that's never going to change. Strategy for a single fixed implementation is indirection with no payoff.
10. Observer
One-to-many notification. Django signals (post_save, pre_delete), event-driven UIs, real-time notification systems.
from abc import ABC, abstractmethod
class Observer(ABC):
@abstractmethod
def update(self, event: str, data: dict): pass
class Subject:
def __init__(self):
self._observers: list[Observer] = []
def attach(self, observer: Observer): self._observers.append(observer)
def detach(self, observer: Observer): self._observers.remove(observer)
def notify(self, event: str, data: dict):
for observer in self._observers:
observer.update(event, data)
class EmailNotifier(Observer):
def update(self, event: str, data: dict):
print(f"Email sent for event '{event}': {data}")
class LogNotifier(Observer):
def update(self, event: str, data: dict):
print(f"Log recorded for event '{event}': {data}")
order_system = Subject()
order_system.attach(EmailNotifier())
order_system.attach(LogNotifier())
order_system.notify("order_placed", {"order_id": 101, "amount": 250.00})
# Email sent for event 'order_placed': {'order_id': 101, 'amount': 250.0}
# Log recorded for event 'order_placed': {'order_id': 101, 'amount': 250.0}
Skip it when: the notification chain becomes so tangled that you can't trace which observer triggered which side effect. Too many observers on a single subject makes debugging disproportionately hard.
Quick Reference
| Pattern | Category | Python native equivalent | Use when |
|---|---|---|---|
| Singleton | Creational |
logging, module-level vars |
Shared resource, single access point |
| Factory | Creational | Functions returning instances | Object type at runtime |
| Abstract Factory | Creational | ABC + concrete factories | Multiple related object families |
| Builder | Creational | Chained calls, dataclass
|
Complex multi-param construction |
| Prototype | Creational | copy.deepcopy() |
Expensive object creation |
| Adapter | Structural | Wrapper classes | Incompatible interface integration |
| Decorator | Structural |
@decorator syntax |
Runtime behavior extension |
| Facade | Structural | Wrapper modules | Simplify complex subsystem access |
| Strategy | Behavioral | Callable injection | Swappable algorithms at runtime |
| Observer | Behavioral | Django signals, asyncio | Event-driven state notification |
Building Python systems that need to stay maintainable as they scale? At Innostax, our Python engineers work across Django, FastAPI, and ML pipelines — and know when a pattern earns its complexity cost. innostax.com/contact
Originally published on the Innostax Engineering Blog.
Which pattern do you see most over-applied in Python codebases you've worked in? Singleton? Observer? Something else entirely? Drop it below. 👇
#python #programming #beginners #webdev
Top comments (0)