Design patterns are reusable solutions to recurring problems in software development. They work like battle-tested architectural blueprints approved by the community, helping you write more organized, flexible, and maintainable code. In Python, these patterns become even more expressive thanks to the language's dynamic features such as first-class functions, decorators, and metaclasses.
In this complete guide, you'll learn the most important design patterns applied to Python: from creational ones like Singleton and Factory to behavioral ones like Strategy and Observer. Each pattern is explained with real code examples you can use immediately in your projects. Let's dive in!
What Are Design Patterns?
The concept of design patterns was popularized by the book Design Patterns: Elements of Reusable Object-Oriented Software, known as Gang of Four (GoF), published in 1994 by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. The original Gang of Four book defined 23 patterns that became a worldwide reference in object-oriented development.
Patterns are divided into three main categories:
- Creational Patterns — deal with object creation in a flexible and decoupled way
- Structural Patterns — organize class and object composition to form larger structures
- Behavioral Patterns — define how objects interact and distribute responsibilities among themselves
The community-maintained python-patterns repository on GitHub contains implementations of dozens of patterns in pure Python and serves as an excellent hands-on reference.
Why Use Design Patterns in Python?
Python is a multi-paradigm language that supports object-oriented, functional, and procedural programming. This flexibility makes implementing design patterns especially elegant. For instance, while Java requires classes and interfaces for the Strategy pattern, Python lets you use plain functions thanks to first-class function support.
The official Python documentation emphasizes that simplicity and readability are core language principles. Design patterns, when applied properly, serve exactly these principles by providing standardized solutions that other developers instantly recognize.
Before diving into the patterns, it helps to have a solid grasp of Object-Oriented Programming in Python, since most GoF patterns rely on concepts like classes, inheritance, polymorphism, and composition.
Creational Patterns
Creational patterns abstract the instantiation process, making a system independent of how its objects are created, composed, and represented. Let's explore the three most widely used in the Python ecosystem.
Singleton
The Singleton pattern ensures a class has only one instance throughout the program's lifetime and provides a global access point to it. It is widely used for managing database connections, application settings, and logging.
In Python, there are several ways to implement Singleton. The most Pythonic approach uses metaclasses or the threading module for thread safety:
import threading
class SingletonMeta(type):
_instances = {}
_lock = threading.Lock()
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
with cls._lock:
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class Settings(metaclass=SingletonMeta):
def init(self):
self.config = {}
def set(self, key, value):
self.config[key] = value
def get(self, key):
return self.config.get(key)
Testing Singleton
s1 = Settings()
s2 = Settings()
s1.set("debug", True)
print(s2.get("debug")) # True — same instance
print(s1 is s2) # True
An even simpler alternative in Python is to use modules as singletons. Since Python modules are loaded only once, any variable defined at module level acts as a natural singleton:
# settings.py
class _Settings:
def __init__(self):
self.debug = False
self.database_url = "sqlite:///app.db"
settings = _Settings() # imported once, single instance
The Real Python Singleton tutorial explores other approaches including decorators and the monostate pattern.
Factory Method
The Factory Method defines an interface for creating objects but lets subclasses decide which class to instantiate. It is useful when the exact object type is only known at runtime.
from abc import ABC, abstractmethod
class Notifier(ABC):
@abstractmethod
def send(self, message: str) -> bool:
pass
class EmailNotifier(Notifier):
def send(self, message: str) -> bool:
print(f"Sending email: {message}")
return True
class SMSNotifier(Notifier):
def send(self, message: str) -> bool:
print(f"Sending SMS: {message}")
return True
class PushNotifier(Notifier):
def send(self, message: str) -> bool:
print(f"Sending push: {message}")
return True
class NotifierFactory:
@staticmethod
def create(notifier_type: str) -> Notifier:
types = {
"email": EmailNotifier,
"sms": SMSNotifier,
"push": PushNotifier,
}
cls = types.get(notifier_type)
if not cls:
raise ValueError(f"Unknown type: {notifier_type}")
return cls()
Usage
notifier = NotifierFactory.create("email")
notifier.send("Welcome to the system!")
Using the abc module (Abstract Base Classes) from the standard library is recommended for defining clear contracts. The official abc module documentation details how to create abstract classes elegantly in Python.
Builder
The Builder pattern separates the construction of a complex object from its final representation, allowing the same construction process to create different representations. It is ideal for objects with many optional parameters or complex configuration.
class Pizza:
def __init__(self):
self.size = None
self.dough = None
self.toppings = []
self.extra_cheese = False
self.stuffed_crust = False
def __str__(self):
return (f"Pizza {self.size}, {self.dough} dough, "
f"toppings: {', '.join(self.toppings)}, "
f"extra cheese: {self.extra_cheese}, "
f"stuffed crust: {self.stuffed_crust}")
class PizzaBuilder:
def init(self):
self._pizza = Pizza()
def with_size(self, size: str):
self._pizza.size = size
return self
def with_dough(self, dough: str):
self._pizza.dough = dough
return self
def add_topping(self, topping: str):
self._pizza.toppings.append(topping)
return self
def with_extra_cheese(self):
self._pizza.extra_cheese = True
return self
def with_stuffed_crust(self):
self._pizza.stuffed_crust = True
return self
def build(self) -> Pizza:
return self._pizza
Usage
pizza = (PizzaBuilder()
.with_size("Large")
.with_dough("Thin")
.add_topping("Mozzarella")
.add_topping("Pepperoni")
.add_topping("Olives")
.with_extra_cheese()
.build())
print(pizza)
Notice how the Builder returns self from each method, enabling fluent method chaining — a technique that makes the code much more expressive.
Structural Patterns
Structural patterns explain how to assemble objects and classes into larger structures while keeping flexibility and efficiency. Let's cover the most relevant ones for Python.
Adapter
The Adapter allows objects with incompatible interfaces to collaborate. It acts as a wrapper that translates calls from one interface to another — exactly like a power plug adapter in the real world.
class LegacySystem:
def legacy_request(self, data: dict) -> str:
return f"Processing {data.get('name')} via legacy system"
class ModernSystem:
def modern_request(self, name: str, version: int) -> str:
return f"Processing {name} v{version} via modern system"
class SystemAdapter:
def init(self, modern_system: ModernSystem):
self._system = modern_system
def legacy_request(self, data: dict) -> str:
return self._system.modern_request(
name=data.get("name", ""),
version=data.get("version", 1)
)
Usage
legacy = LegacySystem()
modern = ModernSystem()
adapter = SystemAdapter(modern)
print(legacy.legacy_request({"name": "Python"}))
print(adapter.legacy_request({"name": "Python", "version": 3}))
The Refactoring Guru guide on Adapter provides diagrams and visual explanations that help understand this pattern more deeply.
Decorator
Don't confuse this with Python decorators! The structural Decorator pattern lets you attach new behaviors to objects dynamically by wrapping them in decorator objects. Python implements this pattern natively through language-level decorators, which are syntactic sugar for functions that take and return functions.
import functools
import time
def log_execution(func):
@functools.wraps(func)
def wrapper(*args, *kwargs):
print(f"Executing {func.name}...")
result = func(args, **kwargs)
print(f"Finished {func.name}")
return result
return wrapper
def measure_time(func):
@functools.wraps(func)
def wrapper(*args, *kwargs):
start = time.time()
result = func(args, **kwargs)
duration = time.time() - start
print(f"{func.name} took {duration:.3f}s")
return result
return wrapper
@log_execution
@measure_time
def process_data(filename: str) -> list:
time.sleep(0.5)
return [f"Data from {filename}"]
result = process_data("clients.csv")
Stacking multiple decorators in Python is a direct application of the Decorator pattern, letting you compose behaviors in an extremely elegant and reusable way.
Facade
The Facade provides a simplified interface to a complex system. Think of it as the information desk at a mall: you don't need to know the internal details of every store — just ask at the desk.
class Authentication:
def login(self, username: str, password: str) -> bool:
print(f"Authenticating {username}...")
return True
class Payment:
def process(self, amount: float) -> bool:
print(f"Processing payment of ${amount:.2f}...")
return True
class Shipping:
def schedule(self, address: str) -> str:
code = f"SHIP-{hash(address) % 10000:04d}"
print(f"Shipping scheduled to {address} — code {code}")
return code
class StoreFacade:
def init(self):
self._auth = Authentication()
self._payment = Payment()
self._shipping = Shipping()
def purchase_product(self, username: str, password: str,
amount: float, address: str) -> str:
if not self._auth.login(username, password):
return "Authentication failed"
if not self._payment.process(amount):
return "Payment failed"
code = self._shipping.schedule(address)
return f"Purchase complete! Shipping code: {code}"
Usage
store = StoreFacade()
result = store.purchase_product(
"[email protected]", "123456", 149.90, "123 Main St"
)
print(result)
Behavioral Patterns
Behavioral patterns deal with communication between objects, defining how they interact and distribute responsibilities. These patterns have the biggest impact on code flexibility and extensibility.
Strategy
The Strategy pattern defines a family of interchangeable algorithms and lets the algorithm vary independently from the clients that use it. In Python, you can implement Strategy extremely concisely using plain functions.
from typing import Callable
Strategies as functions — pure Python!
def calculate_standard_shipping(weight: float) -> float:
return weight * 1.5 + 10
def calculate_express_shipping(weight: float) -> float:
return weight * 3.0 + 20
def calculate_international_shipping(weight: float) -> float:
return weight 5.0 + 50 + weight 0.1
class ShippingCalculator:
def init(self, strategy: Callable[[float], float]):
self._strategy = strategy
def set_strategy(self, strategy: Callable[[float], float]):
self._strategy = strategy
def calculate(self, weight: float) -> float:
return self._strategy(weight)
Usage
calculator = ShippingCalculator(calculate_standard_shipping)
print(f"Standard shipping: ${calculator.calculate(5):.2f}")
calculator.set_strategy(calculate_express_shipping)
print(f"Express shipping: ${calculator.calculate(5):.2f}")
calculator.set_strategy(calculate_international_shipping)
print(f"International shipping: ${calculator.calculate(5):.2f}")
See how Python simplifies Strategy: in languages like Java or C#, you would need interfaces and separate classes. In Python, first-class functions make the implementation trivial. The Real Python Strategy Pattern article goes deeper into this concept.
Observer
The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified automatically. This is the pattern behind event systems, notifications, and reactive programming.
from abc import ABC, abstractmethod
class Observer(ABC):
@abstractmethod
def update(self, event: str, data: dict) -> None:
pass
class Logger(Observer):
def update(self, event: str, data: dict) -> None:
print(f"[LOG] Event: {event} | Data: {data}")
class EmailService(Observer):
def update(self, event: str, data: dict) -> None:
if event == "user_registered":
print(f"[EMAIL] Welcome email to {data.get('email')}")
class PushNotification(Observer):
def update(self, event: str, data: dict) -> None:
print(f"[PUSH] Notification sent to {data.get('name')}")
class EventManager:
def init(self):
self._observers: list[Observer] = []
def subscribe(self, observer: Observer) -> None:
self._observers.append(observer)
def unsubscribe(self, observer: Observer) -> None:
self._observers.remove(observer)
def notify(self, event: str, data: dict) -> None:
for obs in self._observers:
obs.update(event, data)
Usage
manager = EventManager()
manager.subscribe(Logger())
manager.subscribe(EmailService())
manager.subscribe(PushNotification())
manager.notify("user_registered", {
"name": "John Doe",
"email": "[email protected]"
})
Modern frameworks like Django use the Observer pattern through their signals system, allowing application components to react to events like model saves or user logins.
Command
The Command pattern turns a request into a standalone object that contains all the information needed to perform the action. This enables parameterizing methods, queuing operations, and implementing undo/redo.
from abc import ABC, abstractmethod
class Command(ABC):
@abstractmethod
def execute(self) -> None:
pass
@abstractmethod
def undo(self) -> None:
pass
class LightCommand(Command):
def init(self, room: str):
self._room = room
self._on = False
def execute(self) -> None:
self._on = True
print(f"Light in {self._room} turned on")
def undo(self) -> None:
self._on = False
print(f"Light in {self._room} turned off")
class RemoteControl:
def init(self):
self._history: list[Command] = []
def execute(self, command: Command) -> None:
command.execute()
self._history.append(command)
def undo(self) -> None:
if self._history:
command = self._history.pop()
command.undo()
Usage
living_room_light = LightCommand("living room")
bedroom_light = LightCommand("bedroom")
remote = RemoteControl()
remote.execute(living_room_light)
remote.execute(bedroom_light)
remote.undo() # Undoes last command
remote.undo() # Undoes second to last
The GeeksforGeeks Command Pattern tutorial for Python offers additional examples and advanced use cases.
Design Patterns with Type Hints
Modern Python offers robust type hint support, which significantly improves design pattern readability and safety. Using type hints lets IDEs and static analysis tools like mypy catch errors before runtime.
If you are not yet comfortable with type hints, check out our complete guide on Python Type Hints before applying the patterns below.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Serializable(Protocol):
def serialize(self) -> str: ...
class JsonSerializer:
def serialize(self) -> str:
return '{"format": "JSON"}'
class XmlSerializer:
def serialize(self) -> str:
return "XML "
def export(serializer: Serializable) -> None:
if isinstance(serializer, Serializable):
print(serializer.serialize())
export(JsonSerializer())
export(XmlSerializer())
Using Protocol (introduced in PEP 544) enables structural typing, where what matters is the object's structure rather than its formal inheritance — a concept that aligns perfectly with Python's duck typing philosophy.
When (Not) to Use Design Patterns
Design patterns are powerful tools, but they should not be applied indiscriminately. Here are some practical guidelines:
- Use patterns when you identify a recurring problem that already has a well-known solution
- Don't force patterns — simple, straightforward code is almost always better than over-engineered code with unnecessary patterns
- Prefer Python-native solutions — often the language itself already provides abstractions that replace traditional patterns (e.g., decorators, generators, context managers)
- Consider your team — patterns are only useful when everyone on the team understands them and agrees on their use
The SourceMaking guide on Design Patterns offers a complementary view with examples in multiple languages and discussions on when each pattern is appropriate.
Conclusion
Design patterns are a fundamental part of every Python developer's toolkit for writing professional, maintainable, and scalable code. In this guide, you learned the most important patterns organized by category:
- Creational: Singleton, Factory Method, and Builder
- Structural: Adapter, Decorator, and Facade
- Behavioral: Strategy, Observer, and Command
Each pattern solves a specific problem and, when applied well, makes your code more flexible, reusable, and easier to understand. Remember: the goal is not to memorize every pattern, but to build a vocabulary of solutions you can apply when the right problem comes along.
To continue your studies, explore the python-patterns repository on GitHub, read the official Python descriptor how-to guide (used in several advanced patterns), and practice refactoring existing code to identify opportunities for applying the patterns you learned here.
Master the patterns, but never forget: the most important pattern is good judgment combined with deep knowledge of the Python language.