🚀AdvancedAdvanced Patterns

Design Patterns in Python

Pythonic implementations of Singleton, Factory, Observer, Strategy, and Command patterns.

Overview

Design patterns are reusable solutions to recurring software design problems. The original "Gang of Four" patterns were written for C++/Java, but Python's first-class functions, decorators, and dynamic nature change how these patterns look and which ones are even needed. Some patterns that require whole classes in Java become single functions or closures in Python. Others (like Singleton) have multiple idiomatic Python implementations. Understanding both the intent of each pattern and the Pythonic way to implement it makes you a more effective designer. This topic covers five high-value patterns: - **Singleton**: only one instance ever exists - **Factory Method / Abstract Factory**: decouple object creation from use - **Observer**: event-driven publish/subscribe - **Strategy**: swap algorithms at runtime - **Command**: encapsulate an action for queuing, undo, or logging

Key Points

  • •Singleton: use a metaclass, module-level variable, or `__new__` override; modules are themselves singletons
  • •Factory Method: a class or function that returns an object; lets subclasses change the concrete type
  • •Abstract Factory: returns families of related objects without specifying concrete classes
  • •Observer: subjects maintain a list of observers and call `update()` on events
  • •Strategy: pass a function or callable as a parameter instead of hard-coding an algorithm
  • •Command: an object that encapsulates a callable + parameters; enables undo/redo stacks
  • •Python's first-class functions often replace the Strategy and Command patterns with simpler code

Examples

Singleton via metaclass

class SingletonMeta(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class AppConfig(metaclass=SingletonMeta):
    def __init__(self, env="production"):
        self.env = env

a = AppConfig("dev")
b = AppConfig("prod")
print(a is b)        # True
print(a.env)         # dev (first call wins)

Observer / Event System

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

    def on(self, event, callback):
        self._listeners.setdefault(event, []).append(callback)
        return self

    def emit(self, event, *args, **kwargs):
        for cb in self._listeners.get(event, []):
            cb(*args, **kwargs)

class Button(EventEmitter):
    def click(self):
        self.emit("click", self)

btn = Button()
btn.on("click", lambda b: print("Handler 1: clicked"))
btn.on("click", lambda b: print("Handler 2: clicked"))
btn.click()

Strategy pattern Pythonic (functions as strategies)

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

def reverse_sort(data):
    return sorted(data, reverse=True)

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

    def sort(self, data):
        return self.strategy(data)

data = [3, 1, 4, 1, 5, 9, 2, 6]
print(Sorter().sort(data))              # default: sorted
print(Sorter(reverse_sort).sort(data))  # custom strategy
print(Sorter(bubble_sort).sort(data))   # another strategy

Command pattern undo/redo stack

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

    def execute(self, command):
        self._history.append(command)
        command.execute(self)

    def undo(self):
        if self._history:
            self._history.pop().undo(self)

class InsertCommand:
    def __init__(self, text):
        self.text = text
    def execute(self, editor):
        editor.text += self.text
    def undo(self, editor):
        editor.text = editor.text[:-len(self.text)]

ed = TextEditor()
ed.execute(InsertCommand("Hello"))
ed.execute(InsertCommand(" World"))
print(ed.text)   # Hello World
ed.undo()
print(ed.text)   # Hello

Common Mistakes

  • Over-engineering: using a full pattern when a plain function or closure suffices (especially Strategy and Command in Python)
  • Singleton without thread safety double-checked locking not needed in Python (GIL helps), but use `threading.Lock` if safe lazy-init matters
  • Observer memory leaks listeners that hold references to objects prevent garbage collection; use `weakref` for long-lived subjects

Note

Before implementing a pattern, ask "what problem am I solving?" Python's functions, closures, and built-ins often make classic OO patterns unnecessary. A Strategy is frequently just a function argument; a Command is often a `functools.partial`.

Exercises

Write Python code to solve each exercise. Run it in your browser and check your output.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

In Python, the simplest Singleton is often a module-level variable. Why?