10 Python Design Patterns to Streamline Your Python Development Projects
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
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to