Design Patterns in Python

Master Singleton, Factory, Observer, Strategy, and Decorator patterns with Pythonic implementations.

Advanced 40 min read 🐍 Python

Why Design Patterns?

Design patterns are proven solutions to common software design problems. They're not code you copy-paste — they're templates for thinking about how to structure your code. Python's dynamic nature means many patterns are simpler than in Java or C++, and some are built right into the language.

Patterns are tools, not rules

Don't force patterns where they don't fit. A simple function is better than a over-engineered pattern. Use patterns when they solve a real problem in your code, not because they're "best practice."

Singleton — One Instance Only

A Singleton ensures only one instance of a class exists. In Python, this is often done with module-level variables (which are naturally singletons) or with __new__:

class DatabaseConnection:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._initialized = False
        return cls._instance

    def __init__(self, host="localhost"):
        if self._initialized:
            return
        self.host = host
        self.connected = True
        self._initialized = True
        print(f"Connected to {host}")

# Both variables point to the SAME instance
db1 = DatabaseConnection("prod-server")
db2 = DatabaseConnection("other-server")  # Ignored — already initialized

print(f"Same instance? {db1 is db2}")
print(f"Host: {db2.host}")
Output
Connected to prod-server
Same instance? True
Host: prod-server

Pythonic alternative: Just use a module variable. Python modules are singletons by default — importing the same module twice gives you the same object.

Factory — Create Without Specifying Class

A Factory creates objects without the caller needing to know the exact class. This is useful when the type depends on input data or configuration:

from abc import ABC, abstractmethod

class Serializer(ABC):
    @abstractmethod
    def serialize(self, data: dict) -> str: ...

class JSONSerializer(Serializer):
    def serialize(self, data):
        import json
        return json.dumps(data, indent=2)

class CSVSerializer(Serializer):
    def serialize(self, data):
        headers = ",".join(data.keys())
        values = ",".join(str(v) for v in data.values())
        return f"{headers}\n{values}"

class XMLSerializer(Serializer):
    def serialize(self, data):
        items = "".join(f"<{k}>{v}" for k, v in data.items())
        return f"{items}"

def get_serializer(format: str) -> Serializer:
    """Factory function — returns the right serializer."""
    serializers = {
        "json": JSONSerializer,
        "csv": CSVSerializer,
        "xml": XMLSerializer,
    }
    if format not in serializers:
        raise ValueError(f"Unknown format: {format}")
    return serializers[format]()

# Usage — caller doesn't know which class is used
data = {"name": "Alice", "age": 30, "city": "NYC"}
for fmt in ["json", "csv", "xml"]:
    s = get_serializer(fmt)
    print(f"--- {fmt.upper()} ---")
    print(s.serialize(data))
Output
--- JSON ---
{
  "name": "Alice",
  "age": 30,
  "city": "NYC"
}
--- CSV ---
name,age,city
Alice,30,NYC
--- XML ---
Alice30NYC

Observer — Event System

The Observer pattern lets objects subscribe to events and get notified when something happens. It decouples the event source from the handlers:

class EventEmitter:
    def __init__(self):
        self._listeners = {}

    def on(self, event: str, callback):
        """Subscribe to an event."""
        self._listeners.setdefault(event, []).append(callback)

    def emit(self, event: str, *args, **kwargs):
        """Notify all subscribers of an event."""
        for callback in self._listeners.get(event, []):
            callback(*args, **kwargs)

# Usage
emitter = EventEmitter()

# Subscribe handlers
emitter.on("user_created", lambda name: print(f"  Welcome email sent to {name}"))
emitter.on("user_created", lambda name: print(f"  Analytics: new user {name}"))
emitter.on("user_created", lambda name: print(f"  Slack notification: {name} joined"))

# Emit event — all handlers run
print("Creating user...")
emitter.emit("user_created", "Alice")
Output
Creating user...
  Welcome email sent to Alice
  Analytics: new user Alice
  Slack notification: Alice joined
Key Takeaway: The Observer pattern decouples "what happened" from "what should happen next." Adding a new action (like sending a Slack message) doesn't require changing the user creation code.

Strategy — Swappable Algorithms

Strategy lets you swap algorithms at runtime. In Python, functions are first-class objects, so this pattern is beautifully simple:

from typing import Callable

# Different pricing strategies
def standard_price(base: float) -> float:
    return base

def premium_discount(base: float) -> float:
    return base * 0.8  # 20% off

def bulk_discount(base: float) -> float:
    return base * 0.6  # 40% off

class ShoppingCart:
    def __init__(self, pricing: Callable[[float], float] = standard_price):
        self.items: list[tuple[str, float]] = []
        self.pricing = pricing  # Strategy!

    def add(self, name: str, price: float):
        self.items.append((name, price))

    def total(self) -> float:
        return sum(self.pricing(price) for _, price in self.items)

# Same cart, different pricing
cart = ShoppingCart(pricing=premium_discount)
cart.add("Laptop", 1000)
cart.add("Mouse", 50)
print(f"Premium total: ${cart.total():.2f}")

cart.pricing = bulk_discount  # Swap strategy at runtime!
print(f"Bulk total: ${cart.total():.2f}")
Output
Premium total: $840.00
Bulk total: $630.00

Decorator Pattern (Not @decorator)

The GoF Decorator pattern wraps an object to add behavior without modifying the original. Don't confuse this with Python's @decorator syntax (which is related but different):

class Coffee:
    def cost(self) -> float:
        return 5.0
    def description(self) -> str:
        return "Coffee"

class MilkDecorator:
    def __init__(self, coffee):
        self._coffee = coffee
    def cost(self) -> float:
        return self._coffee.cost() + 2.0
    def description(self) -> str:
        return self._coffee.description() + " + Milk"

class SugarDecorator:
    def __init__(self, coffee):
        self._coffee = coffee
    def cost(self) -> float:
        return self._coffee.cost() + 0.5
    def description(self) -> str:
        return self._coffee.description() + " + Sugar"

# Build up a drink with decorators
drink = Coffee()
drink = MilkDecorator(drink)
drink = SugarDecorator(drink)
drink = SugarDecorator(drink)  # Extra sugar!

print(f"{drink.description()}: ${drink.cost():.2f}")
Output
Coffee + Milk + Sugar + Sugar: $8.00

Pattern Summary

PatternProblemPythonic Approach
SingletonOne instance onlyModule variable or __new__
FactoryCreate without specifying classFunction with dict of classes
ObserverReact to eventsCallback lists or signals library
StrategySwappable algorithmsPass functions as arguments
DecoratorAdd behavior to objectsWrapper classes or @decorator
IteratorTraverse collectionsBuilt-in with __iter__/__next__
Context ManagerResource managementBuilt-in with with/__enter__/__exit__
🔍 Deep Dive: Python Has Patterns Built In

Many GoF patterns are unnecessary in Python because the language has them built in. Iterator = for loops and generators. Strategy = first-class functions. Template Method = default arguments and inheritance. Command = callable objects. Null Object = None with or operator. Before implementing a pattern, check if Python already provides a simpler way.