Design Pattern Basics
Design patterns are reusable solutions to problems that commonly arise in software design. Like the principle "don't reinvent the wheel," applying proven patterns improves code quality and makes it easier to communicate with teammates.
Singleton Pattern
The Singleton pattern ensures that a class has only one instance and provides a global access point to it. It's used for configuration management, loggers, database connection pools, and more.
Method 1: Overriding __new__
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, name: str = "default"):
# __init__ is called every time, so check if already initialized
if not hasattr(self, "_initialized"):
self.name = name
self._initialized = True
s1 = Singleton("first")
s2 = Singleton("second")
print(s1 is s2) # True — same object
print(s1.name) # first (s2 is not a new instance)
print(id(s1) == id(s2)) # True
Method 2: Module Level (Most Pythonic)
# Separating into a config.py module gives a natural singleton
# Python modules are executed only once on first import, then cached
class AppConfig:
def __init__(self):
self.debug = False
self.db_url = "sqlite:///app.db"
self.secret_key = "change-me"
self.allowed_hosts: list[str] = ["localhost"]
def update(self, **kwargs) -> None:
for key, value in kwargs.items():
if hasattr(self, key):
setattr(self, key, value)
else:
raise KeyError(f"Unknown setting: {key}")
# Create instance at module level — effectively a singleton
config = AppConfig()
# Usage: just import it
# from config import config
# config.debug = True
Method 3: Using a Metaclass
class SingletonMeta(type):
"""Metaclass that gives singleton behavior"""
_instances: dict = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class DatabasePool(metaclass=SingletonMeta):
"""DB connection pool — only one exists in the application"""
def __init__(self, max_connections: int = 10):
self.max_connections = max_connections
self._connections: list = []
print(f"DB connection pool initialized (max {max_connections})")
def get_connection(self):
if len(self._connections) < self.max_connections:
conn = f"conn_{len(self._connections) + 1}"
self._connections.append(conn)
return conn
raise RuntimeError("Connection pool is full.")
def release(self, conn) -> None:
self._connections.remove(conn)
def status(self) -> str:
return f"In use: {len(self._connections)}/{self.max_connections}"
class Logger(metaclass=SingletonMeta):
"""Application-wide logger — singleton"""
def __init__(self):
self._logs: list[str] = []
def info(self, msg: str) -> None:
import datetime
entry = f"[INFO {datetime.datetime.now():%H:%M:%S}] {msg}"
self._logs.append(entry)
print(entry)
def warning(self, msg: str) -> None:
import datetime
entry = f"[WARN {datetime.datetime.now():%H:%M:%S}] {msg}"
self._logs.append(entry)
print(entry)
def get_all(self) -> list[str]:
return list(self._logs)
# Verify singleton behavior
pool1 = DatabasePool(5)
pool2 = DatabasePool(100) # Returns existing instance, argument ignored
print(pool1 is pool2) # True
print(pool1.max_connections) # 5 (unchanged by pool2)
logger1 = Logger()
logger2 = Logger()
logger1.info("Server started")
logger2.warning("Low memory") # Same object as logger1
print(logger1 is logger2) # True
print(len(logger1.get_all())) # 2 (both logged to the same logger)
Factory Pattern
The Factory pattern separates object creation logic so that client code does not need to know about concrete classes.
Simple Factory
from abc import ABC, abstractmethod
class Animal(ABC):
def __init__(self, name: str):
self.name = name
@abstractmethod
def speak(self) -> str: ...
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.name!r})"
class Dog(Animal):
def speak(self) -> str:
return f"{self.name}: Woof!"
class Cat(Animal):
def speak(self) -> str:
return f"{self.name}: Meow~"
class Bird(Animal):
def speak(self) -> str:
return f"{self.name}: Tweet!"
class AnimalFactory:
"""Simple Factory: creates objects via a static method"""
_creators = {
"dog": Dog,
"cat": Cat,
"bird": Bird,
}
@classmethod
def create(cls, animal_type: str, name: str) -> Animal:
creator = cls._creators.get(animal_type.lower())
if creator is None:
available = ", ".join(cls._creators)
raise ValueError(f"Unknown animal: {animal_type!r}. Available types: {available}")
return creator(name)
@classmethod
def register(cls, animal_type: str, creator: type) -> None:
"""Register a new animal type — applies OCP"""
cls._creators[animal_type.lower()] = creator
# Usage
animals = [
AnimalFactory.create("dog", "Rex"),
AnimalFactory.create("cat", "Whiskers"),
AnimalFactory.create("bird", "Tweety"),
]
for animal in animals:
print(animal.speak())
# Adding a new animal — without modifying AnimalFactory code
class Rabbit(Animal):
def speak(self) -> str:
return f"{self.name}: Squeak~"
AnimalFactory.register("rabbit", Rabbit)
bunny = AnimalFactory.create("rabbit", "Fluffy")
print(bunny.speak())
Factory Method Pattern
from abc import ABC, abstractmethod
class Button(ABC):
@abstractmethod
def render(self) -> str: ...
@abstractmethod
def on_click(self) -> str: ...
class WindowsButton(Button):
def render(self) -> str:
return "[ Windows Button ]"
def on_click(self) -> str:
return "Windows click event triggered"
class MacButton(Button):
def render(self) -> str:
return "( Mac Button )"
def on_click(self) -> str:
return "Mac click event triggered"
class WebButton(Button):
def render(self) -> str:
return "<button>Web Button</button>"
def on_click(self) -> str:
return "DOM click event triggered"
class Dialog(ABC):
"""Abstract class defining the Factory Method"""
@abstractmethod
def create_button(self) -> Button:
"""Factory method — implemented by subclasses"""
...
def render_dialog(self) -> str:
"""Uses the button created by the factory method"""
button = self.create_button()
return f"Dialog: {button.render()}"
def handle_click(self) -> str:
button = self.create_button()
return button.on_click()
class WindowsDialog(Dialog):
def create_button(self) -> Button:
return WindowsButton()
class MacDialog(Dialog):
def create_button(self) -> Button:
return MacButton()
class WebDialog(Dialog):
def create_button(self) -> Button:
return WebButton()
import platform
def get_dialog() -> Dialog:
"""Return the appropriate Dialog for the environment"""
system = platform.system()
if system == "Windows":
return WindowsDialog()
elif system == "Darwin":
return MacDialog()
else:
return WebDialog()
dialog = get_dialog()
print(dialog.render_dialog())
print(dialog.handle_click())
Abstract Factory Pattern
from abc import ABC, abstractmethod
# Abstract products
class TextInput(ABC):
@abstractmethod
def render(self) -> str: ...
class Checkbox(ABC):
@abstractmethod
def render(self) -> str: ...
class Button(ABC):
@abstractmethod
def render(self) -> str: ...
# Concrete products (Windows)
class WindowsTextInput(TextInput):
def render(self) -> str:
return "[___ Windows TextInput ___]"
class WindowsCheckbox(Checkbox):
def render(self) -> str:
return "[x] Windows Checkbox"
class WindowsButton(Button):
def render(self) -> str:
return "[ OK ]"
# Concrete products (Mac)
class MacTextInput(TextInput):
def render(self) -> str:
return "( Mac TextInput )"
class MacCheckbox(Checkbox):
def render(self) -> str:
return "(✓) Mac Checkbox"
class MacButton(Button):
def render(self) -> str:
return "( OK )"
# Abstract factory
class UIFactory(ABC):
@abstractmethod
def create_text_input(self) -> TextInput: ...
@abstractmethod
def create_checkbox(self) -> Checkbox: ...
@abstractmethod
def create_button(self) -> Button: ...
# Concrete factories
class WindowsUIFactory(UIFactory):
def create_text_input(self) -> TextInput:
return WindowsTextInput()
def create_checkbox(self) -> Checkbox:
return WindowsCheckbox()
def create_button(self) -> Button:
return WindowsButton()
class MacUIFactory(UIFactory):
def create_text_input(self) -> TextInput:
return MacTextInput()
def create_checkbox(self) -> Checkbox:
return MacCheckbox()
def create_button(self) -> Button:
return MacButton()
class LoginForm:
"""Uses UI components created by the factory — doesn't know concrete classes"""
def __init__(self, factory: UIFactory):
self.username_input = factory.create_text_input()
self.remember_me = factory.create_checkbox()
self.submit_btn = factory.create_button()
def render(self) -> str:
return (f"=== Login ===\n"
f" {self.username_input.render()}\n"
f" {self.remember_me.render()}\n"
f" {self.submit_btn.render()}")
for factory in [WindowsUIFactory(), MacUIFactory()]:
form = LoginForm(factory)
print(form.render())
print()
Observer Pattern
The Observer pattern automatically notifies other objects when an object's state changes. It's used in event systems, GUI updates, notification features, and more.
Basic Implementation
from abc import ABC, abstractmethod
from typing import Any
class Observer(ABC):
@abstractmethod
def update(self, event: str, data: Any) -> None: ...
class Subject:
"""Observed object (Publisher)"""
def __init__(self):
self._observers: dict[str, list[Observer]] = {}
def subscribe(self, event: str, observer: Observer) -> None:
self._observers.setdefault(event, []).append(observer)
def unsubscribe(self, event: str, observer: Observer) -> None:
if event in self._observers:
self._observers[event].remove(observer)
def notify(self, event: str, data: Any = None) -> None:
for observer in self._observers.get(event, []):
observer.update(event, data)
class StockMarket(Subject):
"""Stock market — notifies observers of price changes"""
def __init__(self):
super().__init__()
self._prices: dict[str, float] = {}
def update_price(self, symbol: str, price: float) -> None:
old_price = self._prices.get(symbol, 0)
self._prices[symbol] = price
change = price - old_price
change_pct = (change / old_price * 100) if old_price else 0
self.notify("price_changed", {
"symbol": symbol,
"price": price,
"change": change,
"change_pct": change_pct,
})
def get_price(self, symbol: str) -> float:
return self._prices.get(symbol, 0)
class AlertObserver(Observer):
"""Price change alert"""
def __init__(self, threshold_pct: float = 5.0):
self.threshold = threshold_pct
def update(self, event: str, data: dict) -> None:
if abs(data["change_pct"]) >= self.threshold:
direction = "surged" if data["change"] > 0 else "dropped"
print(f"[Alert] {data['symbol']} {direction}! "
f"${data['price']:,.2f} ({data['change_pct']:+.1f}%)")
class PortfolioObserver(Observer):
"""Portfolio profit/loss tracker"""
def __init__(self, name: str):
self.name = name
self._holdings: dict[str, tuple[int, float]] = {} # {symbol: (qty, buy_price)}
def add_holding(self, symbol: str, quantity: int, buy_price: float) -> None:
self._holdings[symbol] = (quantity, buy_price)
def update(self, event: str, data: dict) -> None:
symbol = data["symbol"]
if symbol in self._holdings:
qty, buy_price = self._holdings[symbol]
pnl = (data["price"] - buy_price) * qty
pnl_pct = (data["price"] - buy_price) / buy_price * 100
print(f"[{self.name}] {symbol}: {pnl:+,.2f} ({pnl_pct:+.1f}%)")
class LogObserver(Observer):
"""Records all price changes"""
def __init__(self):
self._log: list[str] = []
def update(self, event: str, data: dict) -> None:
entry = (f"{data['symbol']}: ${data['price']:,.2f} "
f"({data['change_pct']:+.1f}%)")
self._log.append(entry)
def get_log(self) -> list[str]:
return list(self._log)
# Usage example
market = StockMarket()
alert = AlertObserver(threshold_pct=3.0)
portfolio = PortfolioObserver("Alice")
logger = LogObserver()
portfolio.add_holding("AAPL", 10, 150.0)
portfolio.add_holding("GOOGL", 5, 100.0)
market.subscribe("price_changed", alert)
market.subscribe("price_changed", portfolio)
market.subscribe("price_changed", logger)
# Update prices
market.update_price("AAPL", 160.0)
market.update_price("GOOGL", 90.0)
market.update_price("MSFT", 300.0)
print("\nFull log:")
for entry in logger.get_log():
print(f" {entry}")
Memory-Safe Observer Using weakref
import weakref
from typing import Callable, Any
class EventEmitter:
"""Event emitter using weakref to prevent observer memory leaks"""
def __init__(self):
self._listeners: dict[str, list] = {}
def on(self, event: str, callback: Callable) -> None:
"""Register event listener (uses weakref)"""
if event not in self._listeners:
self._listeners[event] = []
# Use weakref.WeakMethod for methods, weakref.ref for functions
try:
ref = weakref.WeakMethod(callback)
except TypeError:
ref = weakref.ref(callback)
self._listeners[event].append(ref)
def emit(self, event: str, *args: Any, **kwargs: Any) -> None:
"""Emit event — dead weakrefs are automatically removed"""
listeners = self._listeners.get(event, [])
alive = []
for ref in listeners:
callback = ref()
if callback is not None:
callback(*args, **kwargs)
alive.append(ref)
self._listeners[event] = alive
class Button(EventEmitter):
def __init__(self, label: str):
super().__init__()
self.label = label
def click(self) -> None:
print(f"[Button '{self.label}' clicked]")
self.emit("click", self)
class ClickCounter:
def __init__(self, name: str):
self.name = name
self.count = 0
def on_click(self, button: Button) -> None:
self.count += 1
print(f"[{self.name}] '{button.label}' click count: {self.count}")
btn = Button("OK")
counter = ClickCounter("Counter")
btn.on("click", counter.on_click)
btn.click()
btn.click()
# After deleting counter — automatically cleaned up since it's a weakref
del counter
btn.click() # Nothing happens (dead weakref automatically removed)
Strategy Pattern
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Python's first-class functions make this even more concise.
Class-Based Strategy
from abc import ABC, abstractmethod
class SortStrategy(ABC):
@abstractmethod
def sort(self, data: list) -> list: ...
@abstractmethod
def name(self) -> str: ...
class BubbleSort(SortStrategy):
def sort(self, data: list) -> list:
arr = list(data)
n = len(arr)
for i in range(n):
for j in range(n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
return arr
def name(self) -> str:
return "Bubble Sort"
class QuickSort(SortStrategy):
def sort(self, data: list) -> list:
if len(data) <= 1:
return list(data)
pivot = data[len(data) // 2]
left = [x for x in data if x < pivot]
middle = [x for x in data if x == pivot]
right = [x for x in data if x > pivot]
return self.sort(left) + middle + self.sort(right)
def name(self) -> str:
return "Quick Sort"
class MergeSort(SortStrategy):
def sort(self, data: list) -> list:
if len(data) <= 1:
return list(data)
mid = len(data) // 2
left = self.sort(data[:mid])
right = self.sort(data[mid:])
return self._merge(left, right)
def _merge(self, left: list, right: list) -> list:
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
def name(self) -> str:
return "Merge Sort"
class Sorter:
def __init__(self, strategy: SortStrategy):
self._strategy = strategy
def set_strategy(self, strategy: SortStrategy) -> None:
"""Can change strategy at runtime"""
self._strategy = strategy
def sort(self, data: list) -> list:
print(f"[{self._strategy.name()}] Starting sort...")
result = self._strategy.sort(data)
return result
data = [64, 34, 25, 12, 22, 11, 90]
sorter = Sorter(BubbleSort())
print(sorter.sort(data))
sorter.set_strategy(QuickSort())
print(sorter.sort(data))
Functions as Strategies — Python Style
from typing import Callable
# In Python, functions themselves can be used as strategies
SortFunc = Callable[[list], list]
class FlexibleSorter:
def __init__(self, strategy: SortFunc | None = None):
self._strategy = strategy or sorted
def sort(self, data: list) -> list:
return self._strategy(data)
def reverse_sort(data: list) -> list:
return sorted(data, reverse=True)
def sort_by_length(data: list) -> list:
return sorted(data, key=lambda x: len(str(x)))
# Inject functions as strategies
sorter1 = FlexibleSorter() # Default sort
sorter2 = FlexibleSorter(reverse_sort) # Reverse sort
sorter3 = FlexibleSorter(sort_by_length) # Sort by length
sorter4 = FlexibleSorter(lambda d: sorted(d, key=lambda x: -x)) # Lambda also works
nums = [42, 7, 100, 3, 55, 18]
print(sorter1.sort(nums)) # [3, 7, 18, 42, 55, 100]
print(sorter2.sort(nums)) # [100, 55, 42, 18, 7, 3]
print(sorter3.sort(nums)) # [7, 3, 42, 18, 55, 100]
# Real-world: payment processing strategy
class PaymentProcessor:
def __init__(self, strategy: Callable[[float], str]):
self._strategy = strategy
def process(self, amount: float) -> str:
return self._strategy(amount)
def pay_with_card(amount: float) -> str:
return f"Card payment: ${amount:,.2f}"
def pay_with_paypal(amount: float) -> str:
return f"PayPal payment: ${amount:,.2f}"
def pay_with_crypto(amount: float) -> str:
return f"Crypto payment: ${amount:,.2f} (exchange rate applied)"
processor = PaymentProcessor(pay_with_card)
print(processor.process(35.0))
processor._strategy = pay_with_paypal
print(processor.process(35.0))
Decorator Pattern
The Decorator pattern dynamically adds new functionality to an object. Note: this is different from Python's @decorator syntax — they share a name but the GoF pattern and Python language feature are separate concepts.
GoF Decorator Pattern
from abc import ABC, abstractmethod
class Coffee(ABC):
"""Base interface for beverages"""
@abstractmethod
def cost(self) -> float: ...
@abstractmethod
def description(self) -> str: ...
class Espresso(Coffee):
def cost(self) -> float:
return 3.0
def description(self) -> str:
return "Espresso"
class Americano(Coffee):
def cost(self) -> float:
return 4.0
def description(self) -> str:
return "Americano"
class CoffeeDecorator(Coffee, ABC):
"""Base class for decorators"""
def __init__(self, coffee: Coffee):
self._coffee = coffee
def cost(self) -> float:
return self._coffee.cost()
def description(self) -> str:
return self._coffee.description()
class MilkDecorator(CoffeeDecorator):
def cost(self) -> float:
return super().cost() + 0.5
def description(self) -> str:
return f"{super().description()} + Milk"
class SyrupDecorator(CoffeeDecorator):
def __init__(self, coffee: Coffee, syrup_type: str = "Vanilla"):
super().__init__(coffee)
self.syrup_type = syrup_type
def cost(self) -> float:
return super().cost() + 0.7
def description(self) -> str:
return f"{super().description()} + {self.syrup_type} Syrup"
class WhipDecorator(CoffeeDecorator):
def cost(self) -> float:
return super().cost() + 1.0
def description(self) -> str:
return f"{super().description()} + Whipped Cream"
# Order combinations
order1 = Espresso()
order2 = MilkDecorator(Americano())
order3 = WhipDecorator(SyrupDecorator(MilkDecorator(Americano()), "Caramel"))
for order in [order1, order2, order3]:
print(f"{order.description()}: ${order.cost():.2f}")
GoF Decorator via Python Decorators
import functools
import time
from typing import Callable
# Python's @decorator is syntactic sugar for adding functionality to functions
def timer(func: Callable) -> Callable:
"""Decorator that measures function execution time"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"[Timer] {func.__name__}: {elapsed:.4f}s")
return result
return wrapper
def retry(max_attempts: int = 3, exceptions: tuple = (Exception,)):
"""Decorator that retries on failure"""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == max_attempts:
raise
print(f"[Retry] {func.__name__} failed ({attempt}/{max_attempts}): {e}")
return wrapper
return decorator
def cache(func: Callable) -> Callable:
"""Decorator that caches results"""
_cache: dict = {}
@functools.wraps(func)
def wrapper(*args):
if args not in _cache:
_cache[args] = func(*args)
return _cache[args]
wrapper.cache_clear = lambda: _cache.clear()
wrapper.cache_info = lambda: {"size": len(_cache), "keys": list(_cache.keys())}
return wrapper
def validate_types(**type_hints):
"""Decorator that validates argument types"""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs):
import inspect
sig = inspect.signature(func)
bound = sig.bind(*args, **kwargs)
for param_name, expected_type in type_hints.items():
if param_name in bound.arguments:
value = bound.arguments[param_name]
if not isinstance(value, expected_type):
raise TypeError(
f"{param_name} must be {expected_type.__name__}, "
f"got: {type(value).__name__}"
)
return func(*args, **kwargs)
return wrapper
return decorator
# Combining multiple decorators (composable like GoF Decorator)
@timer
@cache
def fibonacci(n: int) -> int:
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
@validate_types(name=str, age=int)
def greet(name: str, age: int) -> str:
return f"Hello, {name}! (age {age})"
@retry(max_attempts=3, exceptions=(ValueError,))
def unstable_operation(value: int) -> int:
import random
if random.random() < 0.5:
raise ValueError("Temporary error!")
return value * 2
print(fibonacci(30))
print(fibonacci(30)) # Returned instantly from cache
print(greet("Alice", 25))
try:
greet("Name", "age") # Raises TypeError
except TypeError as e:
print(f"Type error: {e}")
Pro Tips
1. Criteria for choosing patterns
# Only use a pattern when you have a problem
# Simple case: no pattern needed
def process(data):
return sorted(data)
# Complex case (algorithm needs to be swappable): Strategy pattern
class DataProcessor:
def __init__(self, sort_strategy=sorted):
self.sort = sort_strategy
def process(self, data):
return self.sort(data)
2. Module-level variables as a Singleton alternative
# Module-level variables are simpler and more Pythonic than Singleton classes
# settings.py
DEBUG = False
DATABASE_URL = "sqlite:///app.db"
MAX_CONNECTIONS = 10
# Usage
# import settings
# if settings.DEBUG: ...
3. Automatic Factory registration with __init_subclass__
class Plugin:
_registry: dict[str, type] = {}
def __init_subclass__(cls, plugin_name: str = "", **kwargs):
super().__init_subclass__(**kwargs)
if plugin_name:
Plugin._registry[plugin_name] = cls
print(f"Plugin registered: {plugin_name} → {cls.__name__}")
@classmethod
def create(cls, name: str) -> "Plugin":
if name not in cls._registry:
raise KeyError(f"Unknown plugin: {name}")
return cls._registry[name]()
def run(self) -> str:
raise NotImplementedError
class AudioPlugin(Plugin, plugin_name="audio"):
def run(self) -> str:
return "Processing audio..."
class VideoPlugin(Plugin, plugin_name="video"):
def run(self) -> str:
return "Processing video..."
for name in ["audio", "video"]:
plugin = Plugin.create(name)
print(plugin.run())
Summary
| Pattern | Purpose | Python Notes |
|---|---|---|
| Singleton | Only one instance | Module-level variable is more Pythonic |
| Factory | Separate creation logic | Auto-register with __init_subclass__ |
| Observer | Event notifications | Use weakref to prevent memory leaks |
| Strategy | Swap algorithms | More concise since functions are first-class |
| Decorator | Add functionality | Elegant with @decorator syntax |
Design patterns are means, not ends. Approach them with "this pattern fits this problem" rather than "I want to use this pattern." The best code is the simplest code that solves the problem at hand.