Design Patterns in Python
Learn essential software design patterns in Python — Singleton, Factory, Observer, Strategy, Decorator, and more — with practical examples.
Design patterns are reusable solutions to common software design problems. Python’s dynamic nature makes many patterns simpler than in statically typed languages.
Singleton — One Instance Only
Ensure a class has only one instance (e.g., database connection pool, logger):
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
a = Singleton()
b = Singleton()
assert a is b # True
Pythonic alternative — use a module-level object instead of a class.
Factory — Object Creation
Encapsulate object creation logic:
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def speak(self) -> str: ...
class Dog(Animal):
def speak(self) -> str:
return "Woof!"
class Cat(Animal):
def speak(self) -> str:
return "Meow!"
def animal_factory(species: str) -> Animal:
animals = {"dog": Dog, "cat": Cat}
cls = animals.get(species)
if cls is None:
raise ValueError(f"Unknown species: {species}")
return cls()
pet = animal_factory("dog")
print(pet.speak())
Observer — Event Notification
Define a one-to-many dependency so observers are notified on state changes:
class Subject:
def __init__(self):
self._observers = []
def attach(self, observer):
self._observers.append(observer)
def notify(self, event):
for observer in self._observers:
observer.update(event)
class EmailNotifier:
def update(self, event):
print(f"Email: {event}")
class SlackNotifier:
def update(self, event):
print(f"Slack: {event}")
subject = Subject()
subject.attach(EmailNotifier())
subject.attach(SlackNotifier())
subject.notify("Order #123 placed")
Strategy — Interchangeable Algorithms
Define a family of algorithms and make them interchangeable:
from typing import Protocol
class SortStrategy(Protocol):
def sort(self, data: list) -> list: ...
class BubbleSort:
def sort(self, data: list) -> list:
return sorted(data) # simplified
class QuickSort:
def sort(self, data: list) -> list:
if len(data) <= 1:
return 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)
class Sorter:
def __init__(self, strategy: SortStrategy):
self.strategy = strategy
def sort(self, data: list) -> list:
return self.strategy.sort(data)
sorter = Sorter(QuickSort())
print(sorter.sort([3, 1, 4, 1, 5]))
Decorator Pattern — Extend Behavior
Add responsibilities to objects dynamically (distinct from Python @decorator syntax):
class Coffee:
def cost(self) -> float:
return 2.0
def description(self) -> str:
return "Coffee"
class MilkAddon:
def __init__(self, beverage):
self._beverage = beverage
def cost(self) -> float:
return self._beverage.cost() + 0.5
def description(self) -> str:
return self._beverage.description() + ", milk"
drink = MilkAddon(Coffee())
print(f"{drink.description()} — ${drink.cost()}")
Repository Pattern — Data Access Abstraction
Separate business logic from data storage:
from abc import ABC, abstractmethod
class UserRepository(ABC):
@abstractmethod
def get_by_id(self, user_id: int): ...
@abstractmethod
def save(self, user): ...
class PostgresUserRepository(UserRepository):
def get_by_id(self, user_id: int):
# SQL query here
...
def save(self, user):
...
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
def get_user(self, user_id: int):
return self.repo.get_by_id(user_id)
When to Apply Patterns
| Pattern | Use When |
|---|---|
| Singleton | Exactly one shared instance needed |
| Factory | Object type determined at runtime |
| Observer | Multiple components react to events |
| Strategy | Algorithm varies independently |
| Repository | Swap data sources without changing logic |
Don’t force patterns everywhere — apply them when they reduce complexity, not increase it.