Skip to content
Codeloom
Python

Python Design Patterns: Strategy, Factory, and Observer

Learn how to implement the Strategy, Factory, and Observer design patterns in Python using idiomatic constructs like first-class functions and protocols.

·5 min read · By Codeloom
Intermediate 12 min read

What you'll learn

  • Why design patterns look different in Python than in Java or C++
  • Implementing the Strategy pattern with first-class functions
  • Building flexible Factory patterns with registry dictionaries
  • Creating the Observer pattern using weakrefs and callbacks

Prerequisites

None — this post is self-contained.

Design patterns solve recurring problems in software design. In statically typed languages like Java, patterns often require elaborate class hierarchies. Python’s dynamic nature and first-class functions let you express the same ideas with far less boilerplate. This article walks through three of the most useful patterns and shows how to write them idiomatically in Python.

Strategy Pattern

The Strategy pattern lets you swap an algorithm at runtime without changing the code that uses it. In Java, you define an interface and create a class for each strategy. In Python, a plain function is often enough.

The Classic Approach

from abc import ABC, abstractmethod

class SortStrategy(ABC):
    @abstractmethod
    def sort(self, data: list) -> list:
        ...

class BubbleSort(SortStrategy):
    def sort(self, data: list) -> list:
        arr = data[:]
        for i in range(len(arr)):
            for j in range(len(arr) - 1 - i):
                if arr[j] > arr[j + 1]:
                    arr[j], arr[j + 1] = arr[j + 1], arr[j]
        return arr

class QuickSort(SortStrategy):
    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)

The Pythonic Approach

Since strategies are single-method objects, you can replace the entire hierarchy with a callable.

from typing import Callable

SortFunc = Callable[[list], list]

def bubble_sort(data: list) -> list:
    arr = data[:]
    for i in range(len(arr)):
        for j in range(len(arr) - 1 - i):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
    return arr

class Sorter:
    def __init__(self, strategy: SortFunc):
        self._strategy = strategy

    def sort(self, data: list) -> list:
        return self._strategy(data)

# Usage
sorter = Sorter(bubble_sort)
sorter.sort([3, 1, 2])  # [1, 2, 3]

# Swap strategy at runtime
sorter = Sorter(sorted)  # Built-in works too

The key insight is that any callable with the right signature is a valid strategy. You do not need an interface or base class.

Factory Pattern

The Factory pattern centralizes object creation so that callers do not need to know concrete class names. Python dictionaries make this clean and extensible.

Registry-Based Factory

from dataclasses import dataclass
from typing import Protocol

class Serializer(Protocol):
    def serialize(self, data: dict) -> str: ...

@dataclass
class JsonSerializer:
    indent: int = 2

    def serialize(self, data: dict) -> str:
        import json
        return json.dumps(data, indent=self.indent)

@dataclass
class XmlSerializer:
    root_tag: str = "root"

    def serialize(self, data: dict) -> str:
        elements = "".join(
            f"  <{k}>{v}</{k}>\n" for k, v in data.items()
        )
        return f"<{self.root_tag}>\n{elements}</{self.root_tag}>"

class CsvSerializer:
    def serialize(self, data: dict) -> str:
        header = ",".join(data.keys())
        values = ",".join(str(v) for v in data.values())
        return f"{header}\n{values}"

# The registry
_SERIALIZERS: dict[str, type[Serializer]] = {
    "json": JsonSerializer,
    "xml": XmlSerializer,
    "csv": CsvSerializer,
}

def create_serializer(format: str, **kwargs) -> Serializer:
    cls = _SERIALIZERS.get(format)
    if cls is None:
        raise ValueError(
            f"Unknown format '{format}'. "
            f"Available: {list(_SERIALIZERS.keys())}"
        )
    return cls(**kwargs)

# Usage
s = create_serializer("json", indent=4)
print(s.serialize({"name": "Ada", "age": 36}))

Auto-Registration with Decorators

You can make registration automatic so new serializers just need a decorator.

def register_serializer(name: str):
    def decorator(cls):
        _SERIALIZERS[name] = cls
        return cls
    return decorator

@register_serializer("yaml")
class YamlSerializer:
    def serialize(self, data: dict) -> str:
        return "\n".join(f"{k}: {v}" for k, v in data.items())

Observer Pattern

The Observer pattern lets objects subscribe to events and get notified when something changes. Python’s flexibility with callbacks and weak references makes this straightforward.

import weakref
from collections import defaultdict
from typing import Callable, Any

class EventEmitter:
    def __init__(self):
        self._listeners: dict[str, list[Callable]] = defaultdict(list)

    def on(self, event: str, callback: Callable) -> None:
        self._listeners[event].append(callback)

    def off(self, event: str, callback: Callable) -> None:
        self._listeners[event] = [
            cb for cb in self._listeners[event] if cb != callback
        ]

    def emit(self, event: str, *args: Any, **kwargs: Any) -> None:
        for callback in self._listeners[event]:
            callback(*args, **kwargs)

Using the EventEmitter

class OrderService:
    def __init__(self, events: EventEmitter):
        self._events = events

    def place_order(self, order_id: str, total: float) -> None:
        print(f"Order {order_id} placed for ${total:.2f}")
        self._events.emit("order_placed", order_id, total)

# Subscribers
def send_confirmation(order_id: str, total: float) -> None:
    print(f"Email sent for order {order_id}")

def update_inventory(order_id: str, total: float) -> None:
    print(f"Inventory updated for order {order_id}")

events = EventEmitter()
events.on("order_placed", send_confirmation)
events.on("order_placed", update_inventory)

service = OrderService(events)
service.place_order("ORD-001", 99.99)

Typed Events with dataclasses

For larger systems, you can use dataclasses as event payloads to get type safety.

from dataclasses import dataclass

@dataclass(frozen=True)
class OrderPlaced:
    order_id: str
    total: float
    customer_email: str

class TypedEventEmitter:
    def __init__(self):
        self._listeners: dict[type, list[Callable]] = defaultdict(list)

    def on(self, event_type: type, callback: Callable) -> None:
        self._listeners[event_type].append(callback)

    def emit(self, event: object) -> None:
        for callback in self._listeners[type(event)]:
            callback(event)

emitter = TypedEventEmitter()
emitter.on(OrderPlaced, lambda e: print(f"Got order {e.order_id}"))
emitter.emit(OrderPlaced("ORD-002", 49.99, "user@example.com"))

When to Use Which Pattern

PatternUse When
StrategyYou need to swap algorithms or behaviors at runtime
FactoryObject creation logic is complex or needs to be decoupled from usage
ObserverMultiple components need to react to events without tight coupling

Key Takeaways

Python’s first-class functions, duck typing, and protocols mean you rarely need the heavyweight class hierarchies that patterns require in other languages. A Strategy can be a plain function. A Factory can be a dictionary lookup. An Observer can be a list of callables. Start with the simplest approach, and reach for classes only when you need state or multiple methods on the strategy object.