Skip to main content
Python advanced Lesson 20 of 28

Python Design Patterns

Implement classic design patterns in Python: singleton, factory, observer, strategy, and decorator with idiomatic Pythonic approaches.

Singleton Pattern

Ensures only one instance of a class exists.

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, value=None):
        # Guard against re-initialization
        if not hasattr(self, "_initialized"):
            self.value = value
            self._initialized = True

s1 = Singleton("first")
s2 = Singleton("second")
s1 is s2      # True
s1.value      # "first" — init only ran once

Thread-Safe Singleton

import threading

class ThreadSafeSingleton:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:  # double-checked locking
                    cls._instance = super().__new__(cls)
        return cls._instance

Pythonic Alternative: Module-Level Instance

# config.py
class Config:
    def __init__(self):
        self.debug = False
        self.database_url = "sqlite:///app.db"

# Single instance created once at import time
config = Config()

# Other modules
from config import config
config.debug = True

Factory Pattern

Creates objects without specifying the exact class.

from __future__ import annotations
from abc import ABC, abstractmethod

class Notification(ABC):
    @abstractmethod
    def send(self, message: str) -> None:
        pass

class EmailNotification(Notification):
    def __init__(self, address: str):
        self.address = address

    def send(self, message: str) -> None:
        print(f"Email to {self.address}: {message}")

class SMSNotification(Notification):
    def __init__(self, phone: str):
        self.phone = phone

    def send(self, message: str) -> None:
        print(f"SMS to {self.phone}: {message}")

class PushNotification(Notification):
    def send(self, message: str) -> None:
        print(f"Push: {message}")

# Factory function — simpler than a factory class in Python
def create_notification(type_: str, **kwargs) -> Notification:
    registry = {
        "email": EmailNotification,
        "sms": SMSNotification,
        "push": PushNotification,
    }
    if type_ not in registry:
        raise ValueError(f"Unknown notification type: {type_}")
    return registry[type_](**kwargs)

notif = create_notification("email", address="[email protected]")
notif.send("Your order shipped!")

Observer Pattern

Lets objects subscribe to events fired by another object.

from typing import Callable, Any

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

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

    def off(self, event: str, listener: Callable) -> None:
        self._listeners.get(event, []).remove(listener)

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

# Usage
class OrderService(EventEmitter):
    def place_order(self, order: dict) -> None:
        # ... process order ...
        self.emit("order_placed", order)
        self.emit("inventory_updated", order["items"])

service = OrderService()
service.on("order_placed", lambda o: print(f"Sending confirmation for {o['id']}"))
service.on("order_placed", lambda o: print(f"Charging card for ${o['total']}"))
service.place_order({"id": 42, "total": 99.99, "items": ["book"]})

Strategy Pattern

Defines a family of algorithms and makes them interchangeable.

from typing import Protocol
import heapq

class SortStrategy(Protocol):
    def sort(self, data: list) -> list:
        ...

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 MergeSort:
    def sort(self, data: list) -> list:
        if len(data) <= 1:
            return data
        mid = len(data) // 2
        left = self.sort(data[:mid])
        right = self.sort(data[mid:])
        return self._merge(left, right)

    def _merge(self, left, right):
        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
        return result + left[i:] + right[j:]

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

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

sorter = Sorter(QuickSort())
sorter.sort([5, 3, 1, 4, 2])   # [1, 2, 3, 4, 5]

sorter.strategy = MergeSort()   # swap at runtime
sorter.sort([5, 3, 1, 4, 2])

Decorator Pattern

Wraps an object to add behavior without subclassing.

from abc import ABC, abstractmethod

class DataSource(ABC):
    @abstractmethod
    def write(self, data: str) -> None: ...

    @abstractmethod
    def read(self) -> str: ...

class FileDataSource(DataSource):
    def __init__(self, path: str):
        self.path = path

    def write(self, data: str) -> None:
        with open(self.path, "w") as f:
            f.write(data)

    def read(self) -> str:
        with open(self.path) as f:
            return f.read()

class EncryptionDecorator(DataSource):
    def __init__(self, wrapped: DataSource):
        self._wrapped = wrapped

    def write(self, data: str) -> None:
        self._wrapped.write(self._encrypt(data))

    def read(self) -> str:
        return self._decrypt(self._wrapped.read())

    def _encrypt(self, data: str) -> str:
        return data[::-1]   # trivial example

    def _decrypt(self, data: str) -> str:
        return data[::-1]

class CompressionDecorator(DataSource):
    def __init__(self, wrapped: DataSource):
        self._wrapped = wrapped

    def write(self, data: str) -> None:
        import zlib, base64
        compressed = base64.b64encode(zlib.compress(data.encode())).decode()
        self._wrapped.write(compressed)

    def read(self) -> str:
        import zlib, base64
        raw = self._wrapped.read()
        return zlib.decompress(base64.b64decode(raw)).decode()

# Stack decorators
source = CompressionDecorator(EncryptionDecorator(FileDataSource("data.bin")))
source.write("Hello, World!")
source.read()   # "Hello, World!"

Command Pattern

Encapsulates operations as objects — enables undo/redo and queuing.

from abc import ABC, abstractmethod
from collections import deque

class Command(ABC):
    @abstractmethod
    def execute(self) -> None: ...

    @abstractmethod
    def undo(self) -> None: ...

class TextEditor:
    def __init__(self):
        self.text = ""
        self._history: deque[Command] = deque()

    def execute(self, cmd: Command) -> None:
        cmd.execute()
        self._history.append(cmd)

    def undo(self) -> None:
        if self._history:
            self._history.pop().undo()

class InsertText(Command):
    def __init__(self, editor: TextEditor, text: str):
        self.editor = editor
        self.text = text

    def execute(self) -> None:
        self.editor.text += self.text

    def undo(self) -> None:
        self.editor.text = self.editor.text[:-len(self.text)]

editor = TextEditor()
editor.execute(InsertText(editor, "Hello"))
editor.execute(InsertText(editor, ", World"))
print(editor.text)   # "Hello, World"
editor.undo()
print(editor.text)   # "Hello"

Frequently Asked Questions

Are design patterns necessary in Python?
Many Gang of Four patterns are simpler in Python due to first-class functions, dynamic typing, and duck typing. Some patterns (like Iterator or Strategy) are built into the language. Learn patterns as solutions to problems, not as recipes to apply everywhere.
Is the Singleton pattern bad?
Singletons make testing hard because they carry global state between tests. Prefer dependency injection — pass the shared object explicitly instead of using a global singleton.
What's the difference between the decorator pattern and Python decorators?
The decorator design pattern wraps an object to add behavior (same interface, extra functionality). Python decorators are a language feature that wraps functions or classes — they can implement the decorator pattern, but they're more general.