Python
Why You're Already Using Design Patterns (And Don't Even Know It)
Discover how Python's built-in features naturally implement classic design patterns like Singleton, Factory, Observer, and Strategy - often without you realizing it. Learn practical, Pythonic alternatives to textbook pattern implementations.
June 2026 · 8 min read · 1 views · 0 hearts
Advertisement
Why You're Already Using Design Patterns (And Don't Even Know It)
You've probably written a factory function without realizing it's a "design pattern." You've definitely used a singleton when you imported a module. The truth is, Python developers stumble into patterns naturally because the language itself encourages certain solutions.
Let's cut through the textbook definitions and look at patterns you'll actually use, with real code you can run today.
The Singleton You've Used Since Day One
Python modules themselves are singletons. When you write:
import config
from config import API_KEY
The module config is loaded once and cached. This is the Singleton pattern — ensuring a class has only one instance — baked into Python's import system.
But when you need a true singleton class? Don't write the usual __new__ boilerplate. Use a module-level instance instead:
# database.py
class DatabaseConnection:
def __init__(self):
self.connected = False
def connect(self):
self.connected = True
db = DatabaseConnection() # This IS your singleton
Any module importing db gets the same instance. Simple, Pythonic, zero magic.
The Factory Pattern Without the Ceremony
Java factories often involve abstract classes and interfaces. In Python, just use a function:
def document_parser(filepath):
if filepath.endswith('.pdf'):
return PDFParser(filepath)
elif filepath.endswith('.csv'):
return CSVParser(filepath)
elif filepath.endswith('.json'):
return JSONParser(filepath)
else:
raise ValueError(f"Unsupported format: {filepath}")
parser = document_parser("report.csv")
This is a Factory pattern — and it's clearer than a class-based version. Need more structure? Add a dictionary lookup:
PARSERS = {
'.pdf': PDFParser,
'.csv': CSVParser,
'.json': JSONParser,
}
parser = PARSERS[extension](filepath)
The Observer Pattern with Python's Built-ins
Event-driven code? Don't write an observer framework. Use callable objects and lists:
class NewsPublisher:
def __init__(self):
self.subscribers = []
def subscribe(self, callback):
self.subscribers.append(callback)
def publish(self, article):
for subscriber in self.subscribers:
subscriber(article)
publisher = NewsPublisher()
publisher.subscribe(lambda msg: print(f"Email: {msg}"))
publisher.subscribe(lambda msg: print(f"Slack: {msg}"))
publisher.publish("New release tomorrow!")
Notice this works without inheritance or abstract interfaces. Python's duck typing means any callable object works.
Strategy Pattern with Functions
When you need interchangeable algorithms, reach for functions or classes with a common method:
def priority_strategy(tasks):
return sorted(tasks, key=lambda t: t.priority, reverse=True)
def deadline_strategy(tasks):
return sorted(tasks, key=lambda t: t.deadline)
class TaskScheduler:
def __init__(self, strategy):
self.strategy = strategy
def schedule(self, tasks):
return self.strategy(tasks)
scheduler = TaskScheduler(priority_strategy)
print(scheduler.schedule(my_tasks))
Need to switch strategies? Just pass a different function:
scheduler.strategy = deadline_strategy
The Decorator Pattern (Language Support Edition)
Python's @decorator syntax is literally the Decorator pattern — adding behavior to an object without modifying it:
import time
from functools import wraps
def log_elapsed(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.3f}s")
return result
return wrapper
@log_elapsed
def data_pipeline():
time.sleep(1.5)
return "done"
data_pipeline() # prints: data_pipeline took 1.500s
The @wraps decorator preserves the original function's metadata. Without it, you'd lose the function name and docstring.
When Patterns Backfire in Python
Some traditional patterns cause more pain than gain in Python:
- Abstract Factory: Often just over-engineering. Use dictionaries and functions.
- Builder: The builder pattern makes sense when you need immutable objects with many parameters. In Python, you can often just use keyword arguments with sensible defaults.
- Visitor: This pattern exists to work around languages without multiple dispatch. Python has
functools.singledispatchwhich handles this more cleanly.
Real-World Example: Combining Patterns
Here's a practical scenario that uses three patterns together. A logging system that can format messages different ways (Strategy) and notify multiple handlers (Observer), with a shared configuration (Singleton):
import logging
class LoggerConfig:
"""Module-level singleton via normal import"""
pass
def json_formatter(message):
return f'{{"msg": "{message}"}}'
def plain_formatter(message):
return message
class Logger:
def __init__(self, name, formatter=plain_formatter):
self.name = name
self.formatter = formatter
self.handlers = [] # Observer pattern
def add_handler(self, handler):
self.handlers.append(handler)
def log(self, message):
formatted = self.formatter(f"{self.name}: {message}")
for handler in self.handlers:
handler(formatted)
# Usage
logger = Logger("app", json_formatter)
logger.add_handler(print)
logger.add_handler(lambda m: open("debug.log", "a").write(m + "\n"))
logger.log("user logged in")
This code is production-ready, testable, and extends naturally. Each pattern serves a purpose without adding ceremony.
The Smart Way to Use Patterns
The best pattern is the one you don't notice. Python's readability lets you implement patterns with 10 lines of code where Java might need 50. Always prefer:
- A plain function over a class with one method
- A module-level variable over a singleton class
- A list of callbacks over an event system
When you recognize you're solving a common problem, reach for the simplest Pythonic expression of that pattern. Your future self — and anyone reading your code — will thank you.
Advertisement
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.