Design patterns are proven solutions to common problems. But Python’s flexibility means we often implement them differently than Java or C++. First-class functions, decorators, and duck typing let us skip a lot of the ceremony. Here are the patterns that come up most in Python.
Singleton — One Instance Only
A singleton ensures only one instance of a class exists. In Python, the simplest approach is just a module-level instance. But here’s the __new__ approach for when we need a class:
class Database:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.connection = "connected" # init once
return cls._instance
db1 = Database()
db2 = Database()
print(db1 is db2) # True — same object
The Pythonic way: just use a module. A module is imported once, so module-level variables are natural singletons.
# config.py
settings = {"debug": True, "db_url": "postgres://..."}
# everywhere else: from config import settings
Factory — @classmethod Factories
Factory methods create objects without exposing the creation logic. In Python, @classmethod is the natural fit.
class User:
def __init__(self, name: str, role: str):
self.name = name
self.role = role
@classmethod
def admin(cls, name: str) -> "User":
return cls(name, role="admin")
@classmethod
def from_dict(cls, data: dict) -> "User":
return cls(data["name"], data["role"])
admin = User.admin("Manish")
user = User.from_dict({"name": "Raj", "role": "viewer"})
print(admin.role) # admin
We get named constructors that clearly communicate intent — much better than passing flags to __init__.
Builder — Fluent Method Chaining
The builder pattern constructs complex objects step by step. In Python, we return self from each method to enable chaining.
class QueryBuilder:
def __init__(self):
self._table = ""
self._conditions = []
self._limit = None
def table(self, name: str) -> "QueryBuilder":
self._table = name
return self # return self for chaining
def where(self, condition: str) -> "QueryBuilder":
self._conditions.append(condition)
return self
def limit(self, n: int) -> "QueryBuilder":
self._limit = n
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
sql = QueryBuilder().table("users").where("age > 18").where("active = 1").limit(10).build()
print(sql) # SELECT * FROM users WHERE age > 18 AND active = 1 LIMIT 10
Observer — Event System with Callbacks
The observer pattern lets objects subscribe to events on another object. In Python, we use simple callback lists.
class EventEmitter:
def __init__(self):
self._listeners = {} # event_name -> list of callbacks
def on(self, event: str, callback):
self._listeners.setdefault(event, []).append(callback)
def emit(self, event: str, *args):
for cb in self._listeners.get(event, []):
cb(*args)
emitter = EventEmitter()
emitter.on("user_created", lambda name: print(f"Welcome, {name}!"))
emitter.on("user_created", lambda name: print(f"Sending email to {name}"))
emitter.emit("user_created", "Manish")
# Welcome, Manish!
# Sending email to Manish
Because functions are first-class in Python, we don’t need separate Observer and Subject interfaces. A callback is enough.
Strategy — Functions as Strategies
The strategy pattern swaps algorithms at runtime. In Java, this means interfaces and classes. In Python, we just pass functions.
def bubble_sort(data: list) -> list:
items = data[:]
for i in range(len(items)):
for j in range(len(items) - 1 - i):
if items[j] > items[j + 1]:
items[j], items[j + 1] = items[j + 1], items[j]
return items
def builtin_sort(data: list) -> list:
return sorted(data)
class Sorter:
def __init__(self, strategy=builtin_sort): # default strategy
self.strategy = strategy
def sort(self, data: list) -> list:
return self.strategy(data)
s = Sorter(strategy=bubble_sort) # swap strategy at construction
print(s.sort([3, 1, 2])) # [1, 2, 3]
The only difference from the classic pattern is that we pass a function instead of an object. Python’s first-class functions make the pattern almost invisible.
Decorator — Function and Class-Based
The decorator pattern wraps an object to add behavior. Python has it baked into the language with @decorator syntax.
Function decorator (the most common form):
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.perf_counter() - start:.4f}s")
return result
return wrapper
@timer
def slow_function():
time.sleep(0.1)
slow_function() # slow_function took 0.1002s
Class-based decorator (when we need state):
class CountCalls:
def __init__(self, func):
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"Call #{self.count}")
return self.func(*args, **kwargs)
@CountCalls
def greet(name):
return f"Hello, {name}!"
greet("Manish") # Call #1 → Hello, Manish!
greet("Raj") # Call #2 → Hello, Raj!
print(greet.count) # 2 — state is preserved
Iterator — __iter__ / __next__ and Generators
The iterator pattern provides a way to traverse a collection without exposing its internal structure. Python’s iterator protocol is built on two dunder methods.
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self # the object is its own iterator
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
The Pythonic way: generators do the same thing with way less code.
def countdown(start):
while start > 0:
yield start # pauses here, resumes on next iteration
start -= 1
for num in countdown(3):
print(num) # 3, 2, 1
Generators are lazy iterators built into the language. For most cases, they replace the need for a full iterator class.
Template Method — ABC with Hook Methods
The template method defines the skeleton of an algorithm in a base class, letting subclasses fill in specific steps. Think of it like a form with blanks to fill in.
from abc import ABC, abstractmethod
class DataPipeline(ABC):
def run(self): # template method — defines the steps
data = self.extract()
cleaned = self.transform(data)
self.load(cleaned)
@abstractmethod
def extract(self) -> list: ...
@abstractmethod
def transform(self, data: list) -> list: ...
@abstractmethod
def load(self, data: list) -> None: ...
class CsvPipeline(DataPipeline):
def extract(self) -> list:
return ["raw1", "raw2"] # read from CSV
def transform(self, data: list) -> list:
return [d.upper() for d in data] # clean data
def load(self, data: list) -> None:
print(f"Loaded: {data}") # save to DB
CsvPipeline().run() # Loaded: ['RAW1', 'RAW2']
The run() method is the template. It calls extract, transform, and load in order. Subclasses provide the concrete implementations, but the overall flow stays the same.
Which Pattern, When?
- Singleton — configuration, database connections, caches (but prefer modules)
- Factory — multiple ways to create an object (
from_json,from_csv,admin) - Builder — constructing complex objects step by step (queries, configs)
- Observer — event-driven systems, pub/sub, UI updates
- Strategy — swappable algorithms (sorting, validation, pricing rules)
- Decorator — adding behavior without modifying the original (logging, timing, auth)
- Iterator — traversing collections lazily (prefer generators)
- Template Method — fixed algorithm with customizable steps (ETL, tests, workflows)
In simple language, design patterns are reusable solutions to common problems. Python’s features — first-class functions, decorators, generators, duck typing — let us implement these patterns with far less boilerplate than traditional OOP languages. The pattern is still there; the ceremony isn’t.