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.