Design patterns are reusable solutions to common problems. The good news? Python’s features — first-class functions, duck typing, decorators — make many patterns way simpler than in Java or C++. Some patterns are so baked into the language that we use them without realizing.
Singleton
Ensures only one instance of a class exists. Think of it like a database connection pool — we want exactly one.
class Database:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
db1 = Database()
db2 = Database()
print(db1 is db2) # True — same object
The Pythonic shortcut? Just use a module. Module-level variables are singletons by nature — Python only loads a module once.
Factory
Creates objects without exposing the creation logic. In Python, we often use @classmethod as a factory.
class User:
def __init__(self, name, role):
self.name = name
self.role = role
@classmethod
def admin(cls, name):
return cls(name, role="admin")
@classmethod
def guest(cls, name):
return cls(name, role="guest")
admin = User.admin("Manish") # cleaner than User("Manish", "admin")
guest = User.guest("Visitor")
Observer
When one object changes, all its “watchers” get notified. Think of it like a newsletter — subscribers get updates automatically.
class EventEmitter:
def __init__(self):
self._listeners = {}
def on(self, event, callback):
self._listeners.setdefault(event, []).append(callback)
def emit(self, event, *args):
for cb in self._listeners.get(event, []):
cb(*args)
emitter = EventEmitter()
emitter.on("login", lambda user: print(f"{user} logged in"))
emitter.emit("login", "Manish") # Manish logged in
Strategy
Swap out an algorithm at runtime. In languages like Java, this needs interfaces and classes. In Python, we just pass a function.
def sort_by_name(users):
return sorted(users, key=lambda u: u["name"])
def sort_by_age(users):
return sorted(users, key=lambda u: u["age"])
def display_users(users, strategy):
for user in strategy(users):
print(user)
users = [{"name": "Zara", "age": 25}, {"name": "Aman", "age": 30}]
display_users(users, sort_by_name) # sorted by name
display_users(users, sort_by_age) # sorted by age
In simple language, first-class functions eliminate the need for a whole Strategy class hierarchy.
Decorator Pattern
We already know this one from Python decorators. Wrap a function to extend its behavior without modifying it.
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_calls
def greet(name):
return f"Hello, {name}"
greet("Manish") # prints "Calling greet", then returns "Hello, Manish"
Iterator
Built right into Python. Any object with __iter__ and __next__ is an iterator. We use them every time we write a for loop.
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
val = self.current
self.current -= 1
return val
for num in Countdown(3):
print(num) # 3, 2, 1
Context Manager
The with statement pattern. Handles setup and teardown automatically — great for files, locks, database connections.
class Timer:
def __enter__(self):
import time
self.start = time.time()
return self
def __exit__(self, *args):
import time
print(f"Elapsed: {time.time() - self.start:.2f}s")
with Timer():
sum(range(1_000_000)) # prints elapsed time when block exits
The key takeaway: Python’s dynamic nature — first-class functions, duck typing, protocols — means we get many patterns “for free.” We don’t need heavy class hierarchies when a simple function or module does the job.