Python Tutorial
Design Patterns in Python
Design patterns are proven solutions to recurring design problems, catalogued by the "Gang of Four" in 1994: creating objects flexibly, structuring relationships, and organising behaviour. Many patterns look different — often simpler — in Python, because functions are first-class objects, modules are natural singletons, and duck typing removes the need for many interfaces.
This lesson implements the most useful patterns the Pythonic way: Singleton, Factory, Builder, Adapter, Decorator, Facade, Strategy, Observer and Command, each with a runnable example.
Creational Patterns
Singleton ensures one instance — in Python a module-level object or a cached function is usually enough. Factory creates objects without the caller knowing the concrete class — often a dict mapping names to classes. Builder assembles complex objects step by step, often with method chaining.
Structural Patterns
Adapter wraps an incompatible interface to match the one your code expects. Decorator adds behaviour to objects or functions without changing them — Python's @decorator syntax implements the function version directly. Facade provides one simple interface to a complex subsystem.
Behavioural Patterns
Strategy selects an algorithm at runtime — in Python, just pass a function. Observer notifies subscribers when something happens (event systems, signals). Command wraps an action as an object so it can be queued, logged or undone. Iterator is built into the language through __iter__ and generators.
Use Patterns Sparingly
Patterns are a vocabulary, not a checklist. Apply one when it removes real duplication or coupling; otherwise the simplest code wins.
Examples
Singleton, Factory and Builder
from functools import cache
@cache
def get_settings(): # Pythonic singleton: created once, then reused
print("loading settings")
return {"currency": "INR"}
print(get_settings() is get_settings())
class PdfExporter:
def export(self, data): return f"PDF({len(data)} rows)"
class CsvExporter:
def export(self, data): return f"CSV({len(data)} rows)"
EXPORTERS = {"pdf": PdfExporter, "csv": CsvExporter}
def make_exporter(kind): # Factory
try:
return EXPORTERS[kind]()
except KeyError:
raise ValueError(f"unknown format {kind!r}") from None
print(make_exporter("csv").export([1, 2, 3]))
class QueryBuilder: # Builder with method chaining
def __init__(self, table):
self.table, self.filters, self.limit_n = table, [], None
def where(self, condition):
self.filters.append(condition)
return self
def limit(self, n):
self.limit_n = n
return self
def build(self):
sql = f"SELECT * FROM {self.table}"
if self.filters:
sql += " WHERE " + " AND ".join(self.filters)
if self.limit_n:
sql += f" LIMIT {self.limit_n}"
return sql
print(QueryBuilder("students").where("age > 18").where("city = 'Pune'").limit(10).build())
loading settings
True
CSV(3 rows)
SELECT * FROM students WHERE age > 18 AND city = 'Pune' LIMIT 10
Adapter, Decorator and Facade
import functools
class LegacyPaymentApi: # incompatible interface
def make_payment(self, paise):
return f"legacy paid {paise} paise"
class PaymentAdapter: # Adapter
def __init__(self, legacy):
self.legacy = legacy
def pay(self, rupees):
return self.legacy.make_payment(int(rupees * 100))
print(PaymentAdapter(LegacyPaymentApi()).pay(49.5))
def logged(func): # Decorator
@functools.wraps(func)
def wrapper(*args):
result = func(*args)
print(f"{func.__name__}{args} -> {result}")
return result
return wrapper
@logged
def add(a, b):
return a + b
add(2, 3)
class Inventory:
def reserve(self, item): return f"reserved {item}"
class Billing:
def charge(self, amount): return f"charged {amount}"
class Shipping:
def ship(self, item): return f"shipped {item}"
class CheckoutFacade: # Facade
def __init__(self):
self.inv, self.bill, self.ship = Inventory(), Billing(), Shipping()
def place_order(self, item, amount):
return [self.inv.reserve(item), self.bill.charge(amount), self.ship.ship(item)]
print(CheckoutFacade().place_order("book", 250))
legacy paid 4950 paise
add(2, 3) -> 5
['reserved book', 'charged 250', 'shipped book']
Strategy, Observer and Command
# Strategy: pass the algorithm as a function
def flat_discount(price): return price - 100
def percent_discount(price): return price * 0.9
def no_discount(price): return price
def final_price(price, strategy=no_discount):
return strategy(price)
print([final_price(1000, s) for s in (no_discount, flat_discount, percent_discount)])
# Observer: subscribers are notified of events
class EventBus:
def __init__(self):
self.subscribers = {}
def subscribe(self, event, handler):
self.subscribers.setdefault(event, []).append(handler)
def publish(self, event, data):
for handler in self.subscribers.get(event, []):
handler(data)
bus = EventBus()
bus.subscribe("order_placed", lambda o: print("email sent for", o))
bus.subscribe("order_placed", lambda o: print("stock updated for", o))
bus.publish("order_placed", "WN-101")
# Command: actions as objects that can be undone
class AddText:
def __init__(self, doc, text):
self.doc, self.text = doc, text
def execute(self):
self.doc.append(self.text)
def undo(self):
self.doc.pop()
doc, history = [], []
for word in ["Hello", "Python", "World"]:
cmd = AddText(doc, word)
cmd.execute()
history.append(cmd)
history.pop().undo()
print(doc)
[1000, 900, 900.0]
email sent for WN-101
stock updated for WN-101
['Hello', 'Python']
Common Mistakes
- Porting Java-style patterns literally (interfaces, getInstance()) where a function or module suffices.
- Adding patterns before there is a real need, making simple code complex.
- Implementing Singleton with global mutable state that makes testing hard.
- Confusing the Decorator pattern with Python decorators — related ideas, different scopes.
Key Points to Remember
- Creational: Singleton (module/cached function), Factory (dict of classes), Builder (chaining).
- Structural: Adapter, Decorator (@ syntax), Facade.
- Behavioural: Strategy (pass functions), Observer (event bus), Command (undoable actions).
- Python's first-class functions and duck typing simplify many patterns.
- Use patterns to solve real problems, not by default.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.