Common Design Patterns in Python

Learn the most useful design patterns in Python, how they solve real problems, and when to use each one in your own projects.

8 min read

A design pattern is a reusable solution to a common problem in software design. The idea was popularized by the "Gang of Four" book in 1994, which catalogued 23 patterns for object-oriented programming. Python's dynamic features make many of these patterns simpler than in Java or C++, but the underlying concepts remain valuable.

This article surveys the patterns most useful in everyday Python, with just enough code to show the idea. The next three articles cover factory, singleton, and strategy patterns in depth. If you have read composition over inheritance in Python, you already have the mental model for how patterns compose objects.

Decorator pattern

Python has first-class support for the decorator pattern through the @ syntax. A decorator wraps a function or class to add behavior without modifying the original code. This is the most Pythonic pattern and the one you will use most often.

A timing decorator measures how long a function takes. Write it once and apply it anywhere:

pythonpython
import time
from functools import wraps
 
def timing(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper
 
@timing
def process_large_file(path):
    with open(path) as fh:
        return fh.read()

The @timing syntax is equivalent to process_large_file = timing(process_large_file). The original function is replaced by the wrapped version, which adds timing and delegates to the original. For more on decorators, see Python decorators explained.

Factory pattern

A factory encapsulates object creation so the caller does not need to know which concrete class to instantiate. This is useful when the right class depends on runtime data like a file extension or a configuration value.

Instead of an if-elif chain scattered through the codebase, a single factory function decides which class to create:

pythonpython
class JSONParser:
    def parse(self, content):
        return json.loads(content)
 
class XMLParser:
    def parse(self, content):
        return xmltodict.parse(content)
 
def create_parser(filepath):
    if filepath.endswith(".json"):
        return JSONParser()
    if filepath.endswith(".xml"):
        return XMLParser()
    raise ValueError(f"Unsupported format: {filepath}")

The caller uses create_parser(path) and receives the right parser without knowing or caring which class was chosen. Adding a new format means adding one class and one branch in the factory.

Strategy pattern

The strategy pattern lets you swap algorithms at runtime. Instead of a class with many if-else branches for different behaviors, each behavior is a separate object with a common interface.

A shipping cost calculator that supports multiple carriers uses strategies. Each carrier implements a calculate method, and the order processing code picks the right one:

pythonpython
class FedExStrategy:
    def calculate(self, order):
        return order.weight * 2.5 + 5
 
class UPSStrategy:
    def calculate(self, order):
        return order.weight * 2.0 + 3
 
class ShippingCalculator:
    def __init__(self, strategy):
        self.strategy = strategy
 
    def cost(self, order):
        return self.strategy.calculate(order)

In Python, strategies do not always need classes. A plain function works when the strategy is a single operation. Pass the function directly instead of wrapping it in an object.

Observer pattern

The observer pattern lets one object notify many others when its state changes, without knowing who those observers are. Python's standard library does not include a built-in observer, but the pattern is simple to implement with a list of callbacks.

An event emitter stores listeners and calls them when an event fires:

pythonpython
class EventEmitter:
    def __init__(self):
        self._listeners = {}
 
    def on(self, event, callback):
        self._listeners.setdefault(event, []).append(callback)
 
    def emit(self, event, data=None):
        for callback in self._listeners.get(event, []):
            callback(data)

A logging system registers itself for error events. A metrics system registers for the same events. Neither knows about the other, and the emitter does not know how many listeners exist.

Singleton pattern

The singleton pattern ensures a class has only one instance. In Python, module-level variables often serve this purpose more naturally than a dedicated Singleton class. A module is imported once, and its top-level variables are shared by all importers.

Instead of a Singleton class with private constructors, use a module. Module-level variables are shared by all importers automatically:

pythonpython
# config.py
_settings = {}
 
def load_config(path):
    global _settings
    _settings = read_toml(path)
 
def get(key, default=None):
    return _settings.get(key, default)

Every file that imports config shares the same _settings dictionary. This is simpler than the classical pattern and more idiomatic in Python. The dedicated article on the singleton pattern in Python covers the approach in more detail.

When to use which pattern

Choose a pattern when the problem matches, not because the pattern exists:

  • A decorator is right when you need to add behavior to existing functions.
  • A factory is right when object creation depends on runtime data.
  • A strategy is right when you need swappable algorithms.
  • An observer is right when multiple objects need to react to state changes.

If a pattern feels like it adds more complexity than it removes, it is the wrong pattern for that situation. Simple code with a few if statements is often better than a perfectly architectured pattern that nobody on the team understands.

Rune AI

Rune AI

Key Insights

  • Design patterns are reusable solutions to common software design problems.
  • Python's dynamic features simplify many classical patterns from the Gang of Four.
  • The decorator pattern is built into Python syntax with the @ symbol.
  • Factory patterns encapsulate object creation logic in one place.
  • Strategy and observer patterns become simple with first-class functions.
RunePowered by Rune AI

Frequently Asked Questions

Do I need to learn all 23 Gang of Four patterns?

No. Python's dynamic features make many classical patterns unnecessary or much simpler. Focus on the patterns that solve problems you actually encounter: factory, strategy, singleton, decorator, and observer cover most everyday needs.

Are design patterns the same in Python as in Java?

The concepts are the same, but the implementation is often simpler in Python. First-class functions, duck typing, and dynamic dispatch eliminate much of the boilerplate that Java requires. A pattern that needs three classes in Java might be a single function in Python.

Conclusion

Design patterns are solved problems with names. Learning the names helps you communicate with other developers and recognize when a familiar solution fits. But do not force patterns where they do not belong. Let the problem drive the pattern, not the other way around.