Comprehensive Guide to Design Patterns in Python: Detailed Explanations and Code Examples
Introduction
Welcome to this comprehensive guide on design patterns. Patterns give us shared vocabulary and a handful of repeatable shapes for solving recurring design problems. In a dynamically typed language such as Python, you can often reach for simpler constructs (first-class functions, duck typing, and modules) before adding indirection—so this guide focuses on when a pattern genuinely helps rather than reproducing boilerplate for its own sake.
In this article we cover creational, structural, and behavioural patterns. Each section includes a Python example that favours readability and the loosely typed nature of the language.
Why design patterns in Python?
- Shared language: Terms such as “Factory Method” or “Observer” let you communicate intent quickly.
- Maintainability: Patterns help you keep responsibilities narrow and interfaces stable as codebases grow.
- Pragmatism: Python’s dynamic features can simplify or replace some patterns (e.g. modules make decent singletons), so knowing the trade-offs avoids over-engineering.
Change-driven design checklist (inspired by Practical Object-Oriented Design)
- Start with responsibilities: what role does this object play, and what messages does it send/receive?
- Keep public interfaces small and stable; hide volatility behind them.
- Prefer composition to inheritance unless the hierarchy is truly stable.
- Let change drive abstraction: duplicate → align → extract → name.
- Use tests as feedback—painful setup hints at over-coupling.
- Obey Demeter: avoid reaching through chains; provide narrow helpers or iterators instead.
How to read the examples
- Type hints are used for clarity, but they remain optional at runtime; all examples work without static typing.
- Abstract base classes show intent, yet in production you can often rely on duck typing and simple protocols instead.
- The snippets omit defensive code (error handling, validation) to keep the core idea obvious.
When patterns are overkill in Python
If a pattern adds more ceremony than value, prefer the idiomatic Python approach: pass functions around instead of introducing command objects, or keep a module-level instance instead of a heavyweight singleton. Use these sections as a toolbox, not a checklist.
Hierarchy of Patterns
- Creational Patterns
- Factory Method
- Abstract Factory
- Singleton
- Builder
- Prototype
- Structural Patterns
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Behavioural Patterns
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
1. Creational Patterns
Factory Method
The Factory Method is a creational pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created.
Explanation:
- Intent: Define an interface for creating an object, but let subclasses decide which class to instantiate.
- Problem: A class cannot anticipate the class of objects it needs to create.
- Solution: Create a method in the base class that will create objects. Let derived classes override this method to create objects of specific types.
Pragmatic Python
- When: you have multiple creators sharing setup and expect new product types from different callers or plugins.
- Alternative: use a registry of constructors or pass in a callable instead of subclassing.
- Note: patterns help when you need discoverable extension points or third-party plugins.
Use Case: Document Management System
Imagine you are designing a document management system that can handle different types of documents (e.g., Word, PDF). The Factory Method pattern allows you to create a framework where the specific type of document is created by the subclasses, making it easy to extend the system to support new document types in the future.
Sample Python Code:
from abc import ABC, abstractmethod
# Product Interface
class Document(ABC):
@abstractmethod
def open(self) -> str:
pass
# Concrete Products
class WordDocument(Document):
def open(self) -> str:
return "Opening Word document."
class PDFDocument(Document):
def open(self) -> str:
return "Opening PDF document."
# Creator Interface
class Application(ABC):
@abstractmethod
def create_document(self) -> Document:
pass
def open_document(self) -> str:
document = self.create_document()
result = f"Application: The application is working with {document.open()}"
return result
# Concrete Creators
class WordApplication(Application):
def create_document(self) -> Document:
return WordDocument()
class PDFApplication(Application):
def create_document(self) -> Document:
return PDFDocument()
# Client code
def client_code(application: Application) -> None:
print(f"Client: I'm not aware of the application's class, but it still works.\n"
f"{application.open_document()}")
if __name__ == "__main__":
print("App: Launched with WordApplication.")
client_code(WordApplication())
print("\n")
print("App: Launched with PDFApplication.")
client_code(PDFApplication())
Abstract Factory
The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.
Explanation:
- Intent: Provide an interface for creating families of related or dependent objects.
- Problem: A system should be independent of how its products are created, composed, and represented.
- Solution: Declare an abstract base factory class with methods to create abstract product types.
Pragmatic Python
- When: you already support at least two families of related components and want callers insulated from concrete classes.
- Alternative: inject factory objects or modules; dynamic attributes often suffice.
- Note: keep UI kits or plugins interchangeable without deep inheritance.
Use Case: UI Elements for Different Operating Systems
Imagine you are designing a cross-platform UI library. You want to create UI elements like buttons and checkboxes, but these elements need to look and behave differently on different operating systems (e.g., Windows and MacOS). The Abstract Factory pattern allows you to create these UI elements without coupling your code to specific implementations for each operating system.
Sample Python Code:
from abc import ABC, abstractmethod
# Abstract Product Interfaces
class Button(ABC):
@abstractmethod
def paint(self) -> str:
pass
class Checkbox(ABC):
@abstractmethod
def paint(self) -> str:
pass
# Concrete Products for Windows
class WindowsButton(Button):
def paint(self) -> str:
return "WindowsButton: Painted Windows Button."
class WindowsCheckbox(Checkbox):
def paint(self) -> str:
return "WindowsCheckbox: Painted Windows Checkbox."
# Concrete Products for MacOS
class MacOSButton(Button):
def paint(self) -> str:
return "MacOSButton: Painted MacOS Button."
class MacOSCheckbox(Checkbox):
def paint(self) -> str:
return "MacOSCheckbox: Painted MacOS Checkbox."
# Abstract Factory Interface
class GUIFactory(ABC):
@abstractmethod
def create_button(self) -> Button:
pass
@abstractmethod
def create_checkbox(self) -> Checkbox:
pass
# Concrete Factories for Windows
class WindowsFactory(GUIFactory):
def create_button(self) -> Button:
return WindowsButton()
def create_checkbox(self) -> Checkbox:
return WindowsCheckbox()
# Concrete Factories for MacOS
class MacOSFactory(GUIFactory):
def create_button(self) -> Button:
return MacOSButton()
def create_checkbox(self) -> Checkbox:
return MacOSCheckbox()
# Client Code
def client_code(factory: GUIFactory) -> None:
button = factory.create_button()
checkbox = factory.create_checkbox()
print(button.paint())
print(checkbox.paint())
if __name__ == "__main__":
print("Client: Testing client code with the Windows factory:")
client_code(WindowsFactory())
print("\n")
print("Client: Testing the same client code with the MacOS factory:")
client_code(MacOSFactory())
Singleton
The Singleton pattern ensures that a class has only one instance and provides a global point of access to it.
Explanation:
- Intent: Ensure a class has only one instance, and provide a global point of access to it.
- Problem: Controlling access to a single instance.
- Solution: Hide the constructor and provide a static method to get the instance.
Pragmatic Python
- When: a single shared resource is unavoidable and passing it explicitly is impractical.
- Alternative: use a module-level instance; prefer dependency injection over globals.
- Note: ensure thread safety and testability (e.g. reset hooks).
Use Case: Logger
Imagine you are developing a logging system where you want to ensure that only one instance of the logger exists throughout the application. This Singleton instance can be accessed globally to log messages.
Sample Python Code:
import threading
class SingletonMeta(type):
_instances = {}
_lock = threading.Lock() # Ensures thread-safe initialisation
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
with cls._lock:
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Logger(metaclass=SingletonMeta):
"""Thread-safe singleton logger; expose reset() for tests."""
def __init__(self):
self.log = []
def add_log(self, message: str) -> None:
self.log.append(message)
print(f"Log added: {message}")
def reset(self) -> None:
self.log.clear()
def show_log(self) -> None:
print("Log history:")
for message in self.log:
print(message)
# Client Code
if __name__ == "__main__":
logger1 = Logger()
logger1.add_log("First log message.")
logger1.add_log("Second log message.")
logger2 = Logger()
logger2.add_log("Third log message.")
if id(logger1) == id(logger2):
print("Singleton works, both variables contain the same instance.")
else:
print("Singleton failed, variables contain different instances.")
logger1.show_log()
Builder
The Builder pattern separates the construction of a complex object from its representation so that the same construction process can create different representations.
Explanation:
- Intent: Separate the construction of a complex object from its representation.
- Problem: A complex object needs to be created.
- Solution: Define a Builder class that constructs parts of a Product object step by step.
Pragmatic Python
- When: construction has many optional steps or variants and you reuse the same sequence in different “recipes”.
- Alternative: prefer
dataclassdefaults/validators or helper functions before a formal builder. - Note: use staged construction only when it genuinely simplifies variation.
Use Case: Building a House
Imagine a scenario where we are building different types of houses. A house can have various parts like walls, doors, and windows. The construction process for different types of houses (e.g., a basic house vs. a luxury house) can be different. The Builder pattern allows us to separate the construction process from the final house representation.
Sample Python Code:
from abc import ABC, abstractmethod
# Product
class House:
def __init__(self):
self.parts = []
def add(self, part: str) -> None:
self.parts.append(part)
def list_parts(self) -> None:
print(f"House parts: {', '.join(self.parts)}")
# Builder Interface
class Builder(ABC):
@abstractmethod
def build_walls(self) -> None:
pass
@abstractmethod
def build_doors(self) -> None:
pass
@abstractmethod
def build_windows(self) -> None:
pass
@abstractmethod
def build_roof(self) -> None:
pass
# Concrete Builder
class HouseBuilder(Builder):
def __init__(self) -> None:
self.reset()
def reset(self) -> None:
self._house = House()
def build_walls(self) -> None:
self._house.add("Walls")
def build_doors(self) -> None:
self._house.add("Doors")
def build_windows(self) -> None:
self._house.add("Windows")
def build_roof(self) -> None:
self._house.add("Roof")
def get_product(self) -> House:
house = self._house
self.reset() # Reset the builder for the next product
return house
# Director
class Director:
def __init__(self) -> None:
self._builder = None
@property
def builder(self) -> Builder:
return self._builder
@builder.setter
def builder(self, builder: Builder) -> None:
self._builder = builder
def build_basic_house(self) -> None:
if not self._builder:
raise ValueError("Builder not set")
self.builder.build_walls()
self.builder.build_doors()
def build_luxury_house(self) -> None:
if not self._builder:
raise ValueError("Builder not set")
self.builder.build_walls()
self.builder.build_doors()
self.builder.build_windows()
self.builder.build_roof()
# Client Code
if __name__ == "__main__":
director = Director()
builder = HouseBuilder()
director.builder = builder
print("Building a basic house:")
director.build_basic_house()
basic_house = builder.get_product()
basic_house.list_parts()
print("\nBuilding a luxury house:")
director.build_luxury_house()
luxury_house = builder.get_product()
luxury_house.list_parts()
print("\nBuilding a custom house:")
builder.build_walls()
builder.build_windows()
custom_house = builder.get_product()
custom_house.list_parts()
Prototype
The Prototype pattern is used to create a new object by copying an existing object, known as the prototype.
Explanation:
- Intent: Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
- Problem: When the cost of creating a new object is expensive.
- Solution: Clone the prototype instead of creating new instances from scratch.
Pragmatic Python
- When: cloning expensive-to-create objects is common and you vary a few attributes per clone.
- Alternative: use
copy.copy/copy.deepcopyor explicit factories rather than bespoke registries. - Note: implement
__copy__/__deepcopy__when shallow vs deep matters.
Use Case: Document Management System
Imagine a document management system where creating new documents involves a lot of setup and configuration. Using the Prototype pattern, we can create new documents by copying existing ones, making it easier and faster to create new documents.
Sample Python Code:
import copy
class Prototype:
def __init__(self):
self._objects = {}
def register_object(self, name, obj):
self._objects[name] = obj
def unregister_object(self, name):
if name in self._objects:
del self._objects[name]
def clone(self, name, **attrs):
if name not in self._objects:
raise ValueError(f"Object named '{name}' is not registered.")
obj = copy.deepcopy(self._objects[name])
obj.__dict__.update(attrs)
return obj
# Example Usage
class Document:
def __init__(self, title: str, content: str) -> None:
self.title = title
self.content = content
def __str__(self) -> str:
return f"Document: {self.title}, Content: {self.content}"
if __name__ == "__main__":
prototype = Prototype()
# Register a default document prototype
default_doc = Document("Default Title", "Default Content")
prototype.register_object('default_doc', default_doc)
# Clone the default document with new attributes
new_doc = prototype.clone('default_doc', title="New Title", content="New Content")
print(f"Original document: {default_doc}")
print(f"Cloned document: {new_doc}")
2. Structural Patterns
Adapter
The Adapter pattern allows incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces.
Explanation:
- Intent: Convert the interface of a class into another interface clients expect.
- Problem: Incompatible interfaces.
- Solution: Create an adapter class that translates calls from the interface of a client to the interface of an incompatible class.
Pragmatic Python
- When: you must integrate an incompatible API without changing existing client code, with more integrations expected.
- Alternative: normalise inputs via a helper function or small wrapper before class-based adapters.
- Note: duck typing often removes the need for heavy adapters.
Use Case: Media Player
Imagine you have a media player application that can play audio files. The media player expects all audio files to be in a specific format, but you have a new type of audio file with a different interface. The Adapter pattern allows the media player to play this new type of audio file without modifying the existing media player code.
Sample Python Code:
class MediaPlayer:
def play(self) -> str:
return "MediaPlayer: Playing audio."
class AdvancedAudioPlayer:
def play_advanced_audio(self) -> str:
return "AdvancedAudioPlayer: Playing advanced audio format."
# Adapter Class
class AudioAdapter(MediaPlayer):
def __init__(self, advanced_audio_player: AdvancedAudioPlayer) -> None:
self._advanced_audio_player = advanced_audio_player
def play(self) -> str:
return f"AudioAdapter: {self._advanced_audio_player.play_advanced_audio()}"
# Client Code
def client_code(media_player: MediaPlayer) -> None:
print(media_player.play())
if __name__ == "__main__":
# Using the MediaPlayer directly
print("Client: Using the MediaPlayer directly:")
media_player = MediaPlayer()
client_code(media_player)
print("\n")
# Using the AdvancedAudioPlayer via the Adapter
advanced_audio_player = AdvancedAudioPlayer()
adapter = AudioAdapter(advanced_audio_player)
print("Client: Using the AdvancedAudioPlayer via the Adapter:")
client_code(adapter)
Bridge
The Bridge pattern decouples an abstraction from its implementation so that the two can vary independently.
Explanation:
- Intent: Decouple an abstraction from its implementation.
- Problem: An abstraction and its implementation should not be tightly coupled.
- Solution: Use a bridge to separate the abstraction from its implementation.
Pragmatic Python
- When: two orthogonal dimensions will both grow (e.g. devices and remotes) and you want to avoid combinatorial subclasses.
- Alternative: inject callables or small objects; formalise a bridge only if both axes vary independently.
- Note: swapping callables at runtime often suffices.
Use Case: Remote Controls for Different Devices
Imagine you are designing remote controls for various devices (e.g., TVs and Radios). The functionality of the remote control (e.g., turning on/off, changing channels) can be abstracted from the specific device it controls. The Bridge pattern allows you to separate the abstraction (remote control) from its implementation (the specific device).
Sample Python Code:
from abc import ABC, abstractmethod
# Abstraction
class RemoteControl:
def __init__(self, device: 'Device') -> None:
self._device = device
def turn_on(self) -> str:
return self._device.turn_on()
def turn_off(self) -> str:
return self._device.turn_off()
def set_channel(self, channel: int) -> str:
return self._device.set_channel(channel)
# Refined Abstraction
class AdvancedRemoteControl(RemoteControl):
def mute(self) -> str:
return self._device.mute()
# Implementation Interface
class Device(ABC):
@abstractmethod
def turn_on(self) -> str:
pass
@abstractmethod
def turn_off(self) -> str:
pass
@abstractmethod
def set_channel(self, channel: int) -> str:
pass
@abstractmethod
def mute(self) -> str:
pass
# Concrete Implementations
class TV(Device):
def turn_on(self) -> str:
return "TV: Turning on"
def turn_off(self) -> str:
return "TV: Turning off"
def set_channel(self, channel: int) -> str:
return f"TV: Setting channel to {channel}"
def mute(self) -> str:
return "TV: Muting"
class Radio(Device):
def turn_on(self) -> str:
return "Radio: Turning on"
def turn_off(self) -> str:
return "Radio: Turning off"
def set_channel(self, channel: int) -> str:
return f"Radio: Setting channel to {channel}"
def mute(self) -> str:
return "Radio: Muting"
# Client Code
def client_code(remote: RemoteControl) -> None:
print(remote.turn_on())
print(remote.set_channel(5))
print(remote.turn_off())
def client_code_advanced(remote: AdvancedRemoteControl) -> None:
print(remote.turn_on())
print(remote.set_channel(5))
print(remote.mute())
print(remote.turn_off())
if __name__ == "__main__":
tv = TV()
tv_remote = RemoteControl(tv)
print("Client: Testing TV Remote Control:")
client_code(tv_remote)
print("\n")
radio = Radio()
radio_remote = RemoteControl(radio)
print("Client: Testing Radio Remote Control:")
client_code(radio_remote)
print("\n")
advanced_tv_remote = AdvancedRemoteControl(tv)
print("Client: Testing Advanced TV Remote Control:")
client_code_advanced(advanced_tv_remote)
Composite
The Composite pattern allows you to compose objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly.
Explanation:
- Intent: Compose objects into tree structures to represent part-whole hierarchies.
- Problem: Treating individual objects and compositions of objects uniformly.
- Solution: Define a composite object that can hold individual objects or other composite objects.
Pragmatic Python
- When: you need to treat single items and groups uniformly and run aggregate operations over both.
- Alternative: rely on
__iter__and lists; add an explicit composite only when uniform ops are required. - Demeter: expose needed operations so callers avoid navigating children directly.
Use Case: Organisation Structure
Imagine an organisation where you have employees and managers. Each manager can manage multiple employees and other managers. The Composite pattern allows us to represent this structure in a tree form, where both employees and managers are treated uniformly.
Sample Python Code:
from abc import ABC, abstractmethod
from typing import List, Optional
# Component Interface
class Employee(ABC):
@abstractmethod
def show_details(self) -> str:
pass
def add(self, employee: 'Employee') -> None:
pass
def remove(self, employee: 'Employee') -> None:
pass
def is_composite(self) -> bool:
return False
# Leaf
class Developer(Employee):
def __init__(self, name: str, position: str) -> None:
self.name = name
self.position = position
def show_details(self) -> str:
return f"{self.name} ({self.position})"
# Leaf
class Designer(Employee):
def __init__(self, name: str, position: str) -> None:
self.name = name
self.position = position
def show_details(self) -> str:
return f"{self.name} ({self.position})"
# Composite
class Manager(Employee):
def __init__(self, name: str, position: str) -> None:
self.name = name
self.position = position
self._employees: List[Employee] = []
def add(self, employee: Employee) -> None:
self._employees.append(employee)
def remove(self, employee: Employee) -> None:
self._employees.remove(employee)
def is_composite(self) -> bool:
return True
def show_details(self) -> str:
results = [f"{self.name} ({self.position})"]
for employee in self._employees:
results.append(f" {employee.show_details()}")
return "\n".join(results)
# Client Code
def client_code(employee: Employee) -> None:
print(employee.show_details())
if __name__ == "__main__":
# Leaf nodes
dev1 = Developer("John Doe", "Senior Developer")
dev2 = Developer("Jane Smith", "Junior Developer")
designer = Designer("Emily Davis", "Senior Designer")
# Composite node
manager = Manager("Alice Johnson", "Development Manager")
manager.add(dev1)
manager.add(dev2)
manager.add(designer)
print("Client: I've got a composite manager with employees:")
client_code(manager)
Decorator
The Decorator pattern allows behaviour to be added to an individual object, dynamically, without affecting the behaviour of other objects from the same class.
Explanation:
- Intent: Attach additional responsibilities to an object dynamically.
- Problem: Extending functionality without subclassing.
- Solution: Use composition to add new behaviour.
Pragmatic Python
- When: you need to add responsibilities at runtime without subclass explosion and keep layering flexible.
- Alternative: use function wrappers/higher-order functions; avoid deep decorator class stacks.
- Note: class-based decorators differ from Python’s
@decoratorsyntax; composition stays clearer.
Use Case: Coffee Shop
Imagine a coffee shop where you can order different types of coffee and add various condiments to them. The condiments are like decorators that add functionality (or flavour) to the coffee.
Sample Python Code:
from abc import ABC, abstractmethod
# Component Interface
class Coffee(ABC):
@abstractmethod
def cost(self) -> float:
pass
@abstractmethod
def description(self) -> str:
pass
# Concrete Component
class SimpleCoffee(Coffee):
def cost(self) -> float:
return 5.0 # Base cost of the coffee
def description(self) -> str:
return "Simple Coffee"
# Base Decorator
class CoffeeDecorator(Coffee):
def __init__(self, coffee: Coffee) -> None:
self._coffee = coffee
def cost(self) -> float:
return self._coffee.cost()
def description(self) -> str:
return self._coffee.description()
# Concrete Decorators
class MilkDecorator(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 1.0 # Milk costs extra
def description(self) -> str:
return f"{self._coffee.description()}, Milk"
class SugarDecorator(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.5 # Sugar costs extra
def description(self) -> str:
return f"{self._coffee.description()}, Sugar"
# Client Code
def client_code(coffee: Coffee) -> None:
print(f"Description: {coffee.description()}")
print(f"Cost: {coffee.cost()}")
if __name__ == "__main__":
# Simple Coffee
simple = SimpleCoffee()
print("Client: I've got a simple coffee:")
client_code(simple)
print("\n")
# Coffee with Milk
coffee_with_milk = MilkDecorator(simple)
print("Client: Now I've got a coffee with milk:")
client_code(coffee_with_milk)
print("\n")
# Coffee with Milk and Sugar
coffee_with_milk_and_sugar = SugarDecorator(coffee_with_milk)
print("Client: Now I've got a coffee with milk and sugar:")
client_code(coffee_with_milk_and_sugar)
Facade
The Facade pattern provides a simplified interface to a complex subsystem.
Explanation:
- Intent: Provide a unified interface to a set of interfaces in a subsystem.
- Problem: A system is very complex and difficult to use.
- Solution: Use a facade to provide a simplified interface.
Pragmatic Python
- When: a subsystem overwhelms consumers; expose only the happy-path workflow.
- Alternative: a slim orchestration function/module instead of mirroring every method.
- Dependency: keep a small interface; hide noisy internals to prevent coupling.
Use Case: Home Theatre System
Imagine a home theatre system with several components such as a DVD player, projector, sound system, and lights. Each component has its own interface with multiple methods, making it cumbersome to operate them individually. The Facade pattern can provide a unified interface to simplify these operations.
Sample Python Code:
class DVDPlayer:
def on(self) -> str:
return "DVD Player: On"
def play(self, movie: str) -> str:
return f"DVD Player: Playing '{movie}'"
class Projector:
def on(self) -> str:
return "Projector: On"
def wide_screen_mode(self) -> str:
return "Projector: Set to widescreen mode (16x9 aspect ratio)"
class SoundSystem:
def on(self) -> str:
return "Sound System: On"
def set_volume(self, volume: int) -> str:
return f"Sound System: Volume set to {volume}"
class Lights:
def dim(self, level: int) -> str:
return f"Lights: Dimming to {level}%"
class HomeTheaterFacade:
def __init__(self, dvd: DVDPlayer, projector: Projector, sound: SoundSystem, lights: Lights) -> None:
self._dvd = dvd
self._projector = projector
self._sound = sound
self._lights = lights
def watch_movie(self, movie: str) -> str:
results = []
results.append("Home Theater: Get ready to watch a movie...")
results.append(self._lights.dim(10))
results.append(self._projector.on())
results.append(self._projector.wide_screen_mode())
results.append(self._sound.on())
results.append(self._sound.set_volume(5))
results.append(self._dvd.on())
results.append(self._dvd.play(movie))
return "\n".join(results)
# Client Code
def client_code(facade: HomeTheaterFacade) -> None:
print(facade.watch_movie("Inception"))
if __name__ == "__main__":
dvd = DVDPlayer()
projector = Projector()
sound = SoundSystem()
lights = Lights()
home_theater = HomeTheaterFacade(dvd, projector, sound, lights)
client_code(home_theater)
Flyweight
The Flyweight pattern is used to minimise memory usage or computational expenses by sharing as much as possible with similar objects.
Explanation:
- Intent: Use sharing to support large numbers of fine-grained objects efficiently.
- Problem: Too many objects can consume too much memory.
- Solution: Share common parts of objects instead of creating new ones.
Pragmatic Python
- When: you have masses of near-identical objects and profiling shows memory pressure.
- Alternative: start with caching/interning; use
__slots__/slots=Truedataclasses before flyweights. - Note: only build flyweights if simpler memory fixes aren’t enough.
Use Case: Text Editor Character Rendering
Imagine you are developing a text editor. Each character on the screen can be considered an object. If we create an object for every single character with all its attributes (font, size, colour, position), it would consume a lot of memory. The Flyweight pattern can help by sharing the common attributes among characters.
Sample Python Code:
class Flyweight:
def __init__(self, shared_state: str) -> None:
self._shared_state = shared_state
def operation(self, unique_state: str) -> None:
print(f"Flyweight: Displaying shared ({self._shared_state}) and unique ({unique_state}) state.")
class FlyweightFactory:
_flyweights = {}
def get_flyweight(self, shared_state: str) -> Flyweight:
if shared_state not in self._flyweights:
print(f"FlyweightFactory: Can't find a flyweight, creating new one.")
self._flyweights[shared_state] = Flyweight(shared_state)
else:
print(f"FlyweightFactory: Reusing existing flyweight.")
return self._flyweights[shared_state]
def list_flyweights(self) -> None:
count = len(self._flyweights)
print(f"FlyweightFactory: I have {count} flyweights:")
for key in self._flyweights.keys():
print(key)
# Client Code
def add_character_to_document(factory: FlyweightFactory, character: str, font: str, size: int, colour: str, position: tuple) -> None:
shared_state = f"{character}_{font}_{size}_{colour}"
unique_state = f"Position: {position}"
flyweight = factory.get_flyweight(shared_state)
flyweight.operation(unique_state)
if __name__ == "__main__":
factory = FlyweightFactory()
add_character_to_document(factory, "H", "Arial", 12, "black", (0, 0))
add_character_to_document(factory, "e", "Arial", 12, "black", (1, 0))
add_character_to_document(factory, "l", "Arial", 12, "black", (2, 0))
add_character_to_document(factory, "l", "Arial", 12, "black", (3, 0))
add_character_to_document(factory, "o", "Arial", 12, "black", (4, 0))
add_character_to_document(factory, "H", "Arial", 14, "blue", (0, 1))
add_character_to_document(factory, "e", "Arial", 14, "blue", (1, 1))
add_character_to_document(factory, "l", "Arial", 14, "blue", (2, 1))
add_character_to_document(factory, "l", "Arial", 14, "blue", (3, 1))
add_character_to_document(factory, "o", "Arial", 14, "blue", (4, 1))
factory.list_flyweights()
Proxy
The Proxy pattern provides a surrogate or placeholder for another object to control access to it.
Explanation:
- Intent: Provide a surrogate or placeholder for another object to control access to it.
- Problem: Control access to an object.
- Solution: Use a proxy that delegates the requests to the real subject.
Pragmatic Python
- When: you must interpose access control, caching, or lazy loading without changing clients.
- Alternative: small wrapper functions/descriptors; keep side effects visible (I/O, auth).
- Dependency: depend “northbound” on abstractions; keep the proxy API narrow to avoid train-wreck calls.
Use Case: Access Control for a Secure File Server
Imagine you are developing a secure file server that handles file access operations. You want to ensure that only authorised users can access this server, and you want to log all access attempts. The Proxy pattern can help by providing a proxy that performs access control and logging before delegating requests to the actual file server.
Sample Python Code:
from abc import ABC, abstractmethod
# Abstract File Server Interface
class FileServer(ABC):
@abstractmethod
def access_file(self, filename: str, user: str) -> None:
pass
# Real File Server
class SecureFileServer(FileServer):
def access_file(self, filename: str, user: str) -> None:
print(f"SecureFileServer: {user} is accessing the file {filename}.")
# Proxy
class FileServerProxy(FileServer):
def __init__(self, real_server: SecureFileServer, allowed_users: set[str]) -> None:
self._real_server = real_server
self._allowed_users = allowed_users
def access_file(self, filename: str, user: str) -> None:
if self.check_access(user):
self._real_server.access_file(filename, user)
self.log_access(filename, user)
else:
print(f"FileServerProxy: Access denied for {user}.")
def check_access(self, user: str) -> bool:
print(f"FileServerProxy: Checking access for {user}.")
return user in self._allowed_users
def log_access(self, filename: str, user: str) -> None:
print(f"FileServerProxy: Logging access to the file {filename} by {user}.")
# Client Code
def client_code(file_server: FileServer) -> None:
for user in ("alice", "mallory"):
print(f"\nClient: User {user} attempting to access the file server:")
file_server.access_file("important_document.txt", user)
if __name__ == "__main__":
print("Client: Accessing the file server directly:")
real_server = SecureFileServer() # No access control here
real_server.access_file("important_document.txt", "alice")
print("\n")
print("Client: Accessing the file server through the proxy:")
proxy = FileServerProxy(real_server, allowed_users={"alice"})
client_code(proxy)
3. Behavioural Patterns
Chain of Responsibility
The Chain of Responsibility pattern allows an object to pass the request along a chain of potential handlers until one of them handles the request.
Explanation:
- Intent: Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request.
- Problem: Decouple request senders from receivers.
- Solution: Pass the request along a chain of handlers.
Pragmatic Python
- When: many handlers may or may not handle a request, and you’ll reorder/extend the chain.
- Alternative: generator pipelines or middleware stacks; keep handlers small and stateless.
- Note: small, pure handlers stay easy to test and rearrange.
Use Case: Customer Support Ticket System
Imagine you are developing a customer support ticket system where different levels of support handle different types of issues. Simple issues are handled by a first-level support agent, more complex issues are handled by a second-level support agent, and the most complex issues are handled by a specialist. The Chain of Responsibility pattern allows you to pass the ticket through different support levels until it is handled.
Sample Python Code:
from abc import ABC, abstractmethod
from typing import Optional
# Handler Interface
class Handler(ABC):
@abstractmethod
def set_next(self, handler: 'Handler') -> 'Handler':
pass
@abstractmethod
def handle(self, request: str) -> Optional[str]:
pass
# Abstract Handler
class AbstractHandler(Handler):
_next_handler: Optional[Handler] = None
def set_next(self, handler: Handler) -> Handler:
self._next_handler = handler
return handler
@abstractmethod
def handle(self, request: str) -> Optional[str]:
if self._next_handler:
return self._next_handler.handle(request)
return None
# Concrete Handlers
class FirstLevelSupportHandler(AbstractHandler):
def handle(self, request: str) -> Optional[str]:
if request == "Password reset":
return f"FirstLevelSupport: I'll handle the {request}."
return super().handle(request)
class SecondLevelSupportHandler(AbstractHandler):
def handle(self, request: str) -> Optional[str]:
if request == "Software installation":
return f"SecondLevelSupport: I'll handle the {request}."
return super().handle(request)
class SpecialistSupportHandler(AbstractHandler):
def handle(self, request: str) -> Optional[str]:
if request == "Server outage":
return f"SpecialistSupport: I'll handle the {request}."
return super().handle(request)
# Client Code
def client_code(handler: Handler) -> None:
for issue in ["Password reset", "Software installation", "Server outage", "Unknown issue"]:
print(f"\nClient: Who can handle a {issue}?")
result = handler.handle(issue)
if result:
print(f" {result}")
else:
print(f" {issue} was not handled.")
if __name__ == "__main__":
first_level = FirstLevelSupportHandler()
second_level = SecondLevelSupportHandler()
specialist = SpecialistSupportHandler()
first_level.set_next(second_level).set_next(specialist)
print("Chain: First Level > Second Level > Specialist")
client_code(first_level)
print("\n")
print("Subchain: Second Level > Specialist")
client_code(second_level)
Command
The Command pattern encapsulates a request as an object, thereby allowing for parameterisation of clients with different requests, queuing of requests, and logging the requests.
Explanation:
- Intent: Encapsulate a request as an object.
- Problem: Parameterise clients with different requests, queue requests, and log requests.
- Solution: Create command objects with a common interface to execute the requests.
Pragmatic Python
- When: you need to queue, log, undo/redo, or persist actions separately from the invoker.
- Alternative: plain callables/
functools.partial; use classes when you need metadata or undo info. - Note: callables already give command-like behaviour in Python.
Use Case: Home Automation System
Imagine you are developing a home automation system where you can control various devices like lights, thermostats, and security systems. Each device can have different commands such as turning on, turning off, setting a temperature, etc. The Command pattern allows you to encapsulate these requests as objects and execute them as needed.
Sample Python Code:
from abc import ABC, abstractmethod
# Command Interface
class Command(ABC):
@abstractmethod
def execute(self) -> None:
pass
# Simple Command
class TurnOnLightCommand(Command):
def __init__(self, payload: str) -> None:
self._payload = payload
def execute(self) -> None:
print(f"TurnOnLightCommand: Turning on the light ({self._payload})")
# Complex Command
class AdjustThermostatCommand(Command):
def __init__(self, receiver: 'Thermostat', temperature: int) -> None:
self._receiver = receiver
self._temperature = temperature
def execute(self) -> None:
print("AdjustThermostatCommand: Adjusting the thermostat")
self._receiver.set_temperature(self._temperature)
class Thermostat:
def set_temperature(self, temperature: int) -> None:
print(f"\nThermostat: Setting temperature to {temperature} degrees.")
class Invoker:
_on_start = None
_on_finish = None
def set_on_start(self, command: Command) -> None:
self._on_start = command
def set_on_finish(self, command: Command) -> None:
self._on_finish = command
def do_something_important(self) -> None:
print("Invoker: Does anybody want something done before I begin?")
if isinstance(self._on_start, Command):
self._on_start.execute()
print("Invoker: ...doing something really important...")
print("Invoker: Does anybody want something done after I finish?")
if isinstance(self._on_finish, Command):
self._on_finish.execute()
# Client Code
if __name__ == "__main__":
invoker = Invoker()
invoker.set_on_start(TurnOnLightCommand("Living Room Light"))
thermostat = Thermostat()
invoker.set_on_finish(AdjustThermostatCommand(thermostat, 22))
invoker.do_something_important()
Interpreter
The Interpreter pattern provides a way to evaluate language grammar or expressions. This pattern involves implementing an expression interface which tells to interpret a particular context.
Explanation:
- Intent: Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.
- Problem: Designing a grammar and an interpreter for it.
- Solution: Define classes that represent grammar rules and interpret context.
Pragmatic Python
- When: you have a small, evolving grammar and want each rule to be a testable object.
- Alternative:
argparse,re, parser libraries, or dispatch tables; use expression objects only when composability helps. - Note: interpreter-style objects are rarer in production Python.
Use Case: Simple Chatbot Command Interpretation
Imagine you are developing a simple chatbot that can understand and respond to basic commands. The chatbot needs to evaluate user input based on predefined grammar rules and interpret the commands accordingly. The Interpreter pattern can help by defining these grammar rules and implementing the interpretation logic.
Sample Python Code:
from abc import ABC, abstractmethod
# Abstract Expression
class AbstractExpression(ABC):
@abstractmethod
def interpret(self, context: str) -> bool:
pass
# Terminal Expression
class TerminalExpression(AbstractExpression):
def __init__(self, data: str) -> None:
self._data = data.lower()
def interpret(self, context: str) -> bool:
tokens = context.lower().split()
return self._data in tokens
# Or Expression
class OrExpression(AbstractExpression):
def __init__(self, expr1: AbstractExpression, expr2: AbstractExpression) -> None:
self._expr1 = expr1
self._expr2 = expr2
def interpret(self, context: str) -> bool:
return self._expr1.interpret(context) or self._expr2.interpret(context)
# And Expression
class AndExpression(AbstractExpression):
def __init__(self, expr1: AbstractExpression, expr2: AbstractExpression) -> None:
self._expr1 = expr1
self._expr2 = expr2
def interpret(self, context: str) -> bool:
return self._expr1.interpret(context) and self._expr2.interpret(context)
# Client Code
def client_code() -> None:
# Grammar rules for interpreting commands
greet = TerminalExpression("Hello")
bye = TerminalExpression("Goodbye")
is_greeting = OrExpression(greet, bye)
admin = TerminalExpression("Admin")
password = TerminalExpression("Password")
is_admin_login = AndExpression(admin, password)
# Example input
input1 = "Hello John"
input2 = "Goodbye John"
input3 = "Admin Password"
input4 = "Admin John"
print(f"Is '{input1}' a greeting? {is_greeting.interpret(input1)}")
print(f"Is '{input2}' a greeting? {is_greeting.interpret(input2)}")
print(f"Is '{input3}' an admin login? {is_admin_login.interpret(input3)}")
print(f"Is '{input4}' an admin login? {is_admin_login.interpret(input4)}")
if __name__ == "__main__":
client_code()
Iterator
The Iterator pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
Explanation:
- Intent: Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
- Problem: Need to traverse a collection without exposing its internal structure.
- Solution: Define an iterator class with methods to traverse the collection.
Pragmatic Python
- When: you need custom traversal order/state beyond a simple
forloop. - Alternative: generators for traversal; iterator classes only when stateful/reusable traversals are needed.
- Demeter: expose traversal methods to prevent callers reaching through deep object chains.
Use Case: Traversing a Collection of Words
Imagine you are developing a text processing application that handles a collection of words. You want to be able to traverse this collection in both forward and reverse order without exposing the internal representation of the collection. The Iterator pattern can help by defining an iterator that provides sequential access to the elements.
Sample Python Code:
from collections.abc import Iterator, Iterable
from typing import Any, List, Optional
# Concrete Iterator
class AlphabeticalOrderIterator(Iterator):
"""Explicit iterator; a generator would be terser in production."""
def __init__(self, collection: List[Any], reverse: bool = False) -> None:
self._collection = collection
self._reverse = reverse
self._position = -1 if reverse else 0
def __next__(self) -> Any:
try:
value = self._collection[self._position]
self._position += -1 if self._reverse else 1
except IndexError:
raise StopIteration()
return value
# Concrete Iterable Collection
class WordsCollection(Iterable):
def __init__(self, collection: Optional[List[Any]] = None) -> None:
self._collection = collection or []
def __iter__(self) -> AlphabeticalOrderIterator:
return AlphabeticalOrderIterator(self._collection)
def get_reverse_iterator(self) -> AlphabeticalOrderIterator:
return AlphabeticalOrderIterator(self._collection, True)
def add_item(self, item: Any) -> None:
self._collection.append(item)
def iter_forward(self):
"""Generator-style iteration; idiomatic and succinct."""
yield from self._collection
# Client Code
if __name__ == "__main__":
collection = WordsCollection()
collection.add_item("First")
collection.add_item("Second")
collection.add_item("Third")
print("Straight traversal:")
for item in collection:
print(item)
print("\n")
print("Straight traversal via generator:")
for item in collection.iter_forward():
print(item)
print("\n")
print("Reverse traversal:")
for item in collection.get_reverse_iterator():
print(item)
Mediator
The Mediator pattern defines an object that encapsulates how a set of objects interact. This pattern promotes loose coupling by keeping objects from referring to each other explicitly.
Explanation:
- Intent: Define an object that encapsulates how a set of objects interact.
- Problem: Tight coupling between objects.
- Solution: Use a mediator object to handle interactions.
Pragmatic Python
- When: many peers must coordinate without becoming a mesh of direct calls.
- Alternative: signals/event emitters (
asyncio,blinker); keep the mediator thin and explicit about side effects. - Note: avoid mediators that become another coupling hotspot.
Use Case: Air Traffic Control System
Imagine you are developing an air traffic control system where multiple airplanes (components) need to coordinate their actions such as landing, taking off, and taxiing. Instead of each airplane communicating directly with every other airplane, which can lead to a complex and tightly coupled system, you can use a mediator (the control tower) to manage the interactions.
Sample Python Code:
from __future__ import annotations
from abc import ABC, abstractmethod
# Mediator Interface
class Mediator(ABC):
@abstractmethod
def notify(self, sender: object, event: str) -> None:
pass
# Concrete Mediator
class ControlTower(Mediator):
def __init__(self, airplane1: Airplane1, airplane2: Airplane2) -> None:
self._airplane1 = airplane1
self._airplane1.mediator = self
self._airplane2 = airplane2
self._airplane2.mediator = self
def notify(self, sender: object, event: str) -> None:
if event == "Landing":
print("ControlTower: Airplane1 is landing, informing Airplane2 to hold.")
self._airplane2.hold_position()
elif event == "TakingOff":
print("ControlTower: Airplane2 is taking off, informing Airplane1 to hold.")
self._airplane1.hold_position()
# Base Component
class Airplane:
def __init__(self, mediator: Mediator = None) -> None:
self._mediator = mediator
@property
def mediator(self) -> Mediator:
return self._mediator
@mediator.setter
def mediator(self, mediator: Mediator) -> None:
self._mediator = mediator
# Concrete Components
class Airplane1(Airplane):
def land(self) -> None:
print("Airplane1 is landing.")
self.mediator.notify(self, "Landing")
def hold_position(self) -> None:
print("Airplane1 is holding position.")
class Airplane2(Airplane):
def take_off(self) -> None:
print("Airplane2 is taking off.")
self.mediator.notify(self, "TakingOff")
def hold_position(self) -> None:
print("Airplane2 is holding position.")
# Client Code
if __name__ == "__main__":
airplane1 = Airplane1()
airplane2 = Airplane2()
control_tower = ControlTower(airplane1, airplane2)
print("Client triggers airplane1 to land.")
airplane1.land()
print("\nClient triggers airplane2 to take off.")
airplane2.take_off()
Memento
The Memento pattern provides the ability to restore an object to its previous state. It is used for saving and restoring the internal state of an object.
Explanation:
- Intent: Capture and externalise an object’s internal state without violating encapsulation.
- Problem: Need to restore an object to its previous state.
- Solution: Use a memento object to store the state.
Pragmatic Python
- When: you need undo/rollback for in-memory state while keeping the originator decoupled.
- Alternative: snapshots as dicts or dataclass copies; choose fields deliberately.
- Note: avoid storing external resources inside mementos.
Use Case: Text Editor Undo Functionality
Imagine you are developing a text editor that allows users to write and edit documents. The editor should have an undo feature that lets users revert to previous versions of the document. The Memento pattern can help by capturing the state of the document at various points and allowing the user to restore it.
Sample Python Code:
from datetime import datetime
from typing import List
class Memento:
def __init__(self, state: str) -> None:
self._state = state
self._date = str(datetime.now())[:19]
def get_state(self) -> str:
return self._state
def get_name(self) -> str:
return f"{self._date} / ({self._state})"
def get_date(self) -> str:
return self._date
class Originator:
def __init__(self, state: str) -> None:
self._state = state
self._version = 0
print(f"Originator: My initial state is: {self._state}")
def do_something(self, new_state: Optional[str] = None) -> None:
"""Update state deterministically for easier testing."""
print("Originator: I'm doing something important.")
self._version += 1
self._state = new_state or f"version-{self._version}"
print(f"Originator: and my state has changed to: {self._state}")
def save(self) -> Memento:
return Memento(self._state)
def restore(self, memento: Memento) -> None:
self._state = memento.get_state()
print(f"Originator: My state has changed to: {self._state}")
class Caretaker:
def __init__(self, originator: Originator) -> None:
self._mementos: List[Memento] = []
self._originator = originator
def backup(self) -> None:
print("\nCaretaker: Saving Originator's state...")
self._mementos.append(self._originator.save())
def undo(self) -> None:
if not self._mementos:
return
memento = self._mementos.pop()
print(f"Caretaker: Restoring state to: {memento.get_name()}")
self._originator.restore(memento)
def show_history(self) -> None:
print("Caretaker: Here's the list of mementos:")
for memento in self._mementos:
print(memento.get_name())
# Client Code
if __name__ == "__main__":
originator = Originator("Initial document content.")
caretaker = Caretaker(originator)
caretaker.backup()
originator.do_something()
caretaker.backup()
originator.do_something()
caretaker.backup()
originator.do_something()
print()
caretaker.show_history()
print("\nClient: Now, let's rollback!\n")
caretaker.undo()
print("\nClient: Once more!\n")
caretaker.undo()
Observer
The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
Explanation:
- Intent: Define a one-to-many dependency between objects.
- Problem: Need to notify multiple objects when the state of one object changes.
- Solution: Use an observer interface and concrete observer classes.
Pragmatic Python
- When: multiple listeners must react to publisher changes and you’ll add/remove observers dynamically.
- Alternative: callbacks or signals/
asyncioevents; detach listeners to avoid leaks. - Note: keep the subject’s public API small and message-oriented.
Use Case: Weather Monitoring System
Imagine you are developing a weather monitoring system where multiple display elements (e.g., temperature display, humidity display) need to be updated whenever the weather data changes. The Observer pattern can help by defining a subject (weather station) and observers (display elements) that are notified whenever the weather data changes.
Sample Python Code:
from abc import ABC, abstractmethod
from typing import List
class Observer(ABC):
@abstractmethod
def update(self, weather_data: WeatherData) -> None:
pass
class WeatherData:
def __init__(self) -> None:
self._observers: List[Observer] = []
self._temperature: float = None
self._humidity: float = None
def attach(self, observer: Observer) -> None:
print("WeatherData: Attached an observer.")
self._observers.append(observer)
def detach(self, observer: Observer) -> None:
self._observers.remove(observer)
def notify(self) -> None:
print("WeatherData: Notifying observers...")
for observer in self._observers:
observer.update(self)
def set_measurements(self, temperature: float, humidity: float) -> None:
print(f"\nWeatherData: Setting measurements to Temperature: {temperature}, Humidity: {humidity}")
self._temperature = temperature
self._humidity = humidity
self.notify()
@property
def temperature(self) -> float:
return self._temperature
@property
def humidity(self) -> float:
return self._humidity
class TemperatureDisplay(Observer):
def update(self, weather_data: WeatherData) -> None:
print(f"TemperatureDisplay: The current temperature is {weather_data.temperature}°C")
class HumidityDisplay(Observer):
def update(self, weather_data: WeatherData) -> None:
print(f"HumidityDisplay: The current humidity is {weather_data.humidity}%")
# Client Code
if __name__ == "__main__":
weather_data = WeatherData()
temperature_display = TemperatureDisplay()
weather_data.attach(temperature_display)
humidity_display = HumidityDisplay()
weather_data.attach(humidity_display)
weather_data.set_measurements(25.0, 65.0)
weather_data.set_measurements(30.0, 70.0)
weather_data.detach(temperature_display)
weather_data.set_measurements(20.0, 60.0)
State
The State pattern allows an object to alter its behaviour when its internal state changes. The object will appear to change its class.
Explanation:
- Intent: Allow an object to alter its behaviour when its internal state changes.
- Problem: Need to change the behaviour of an object based on its state.
- Solution: Use state objects to represent different states and change the state object when the state changes.
Pragmatic Python
- When: behaviour truly changes by state, transitions are non-trivial, and more states are likely.
- Alternative: dicts mapping states to callables/methods; use state classes for complex transitions/side effects.
- Note: keeps conditional sprawl in check as states grow.
Use Case: Document Workflow
Imagine you are developing a document management system where a document can be in different states such as Draft, Moderation, and Published. The behaviour of the document changes based on its state. For example, a Draft document can be edited and submitted for moderation, whereas a Published document cannot be edited.
Sample Python Code:
from abc import ABC, abstractmethod
# Context Class
class Document:
_state = None
def __init__(self, state: DocumentState) -> None:
self.transition_to(state)
def transition_to(self, state: DocumentState) -> None:
print(f"Document: Transition to {type(state).__name__}")
self._state = state
self._state.context = self
def edit(self) -> None:
self._state.handle_edit()
def publish(self) -> None:
self._state.handle_publish()
# State Interface
class DocumentState(ABC):
@property
def context(self) -> Document:
return self._context
@context.setter
def context(self, context: Document) -> None:
self._context = context
@abstractmethod
def handle_edit(self) -> None:
pass
@abstractmethod
def handle_publish(self) -> None:
pass
# Concrete States
class DraftState(DocumentState):
def handle_edit(self) -> None:
print("DraftState: Document is being edited.")
def handle_publish(self) -> None:
print("DraftState: Document is submitted for moderation.")
self.context.transition_to(ModerationState())
class ModerationState(DocumentState):
def handle_edit(self) -> None:
print("ModerationState: Cannot edit the document in moderation.")
def handle_publish(self) -> None:
print("ModerationState: Document is published.")
self.context.transition_to(PublishedState())
class PublishedState(DocumentState):
def handle_edit(self) -> None:
print("PublishedState: Cannot edit the published document.")
def handle_publish(self) -> None:
print("PublishedState: Document is already published.")
# Client Code
if __name__ == "__main__":
document = Document(DraftState())
document.edit()
document.publish()
document.edit()
document.publish()
document.edit()
document.publish()
Strategy
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern lets the algorithm vary independently from clients that use it.
Explanation:
- Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable.
- Problem: Need to use different algorithms based on context.
- Solution: Define a strategy interface and concrete strategy classes.
Pragmatic Python
- When: you must swap algorithms at runtime or in tests, with more strategies expected.
- Alternative: inject callables/
partial; use classes when strategies need configuration or shared state. - Note: keeps algorithm choice decoupled from callers.
Use Case: Sorting Algorithms
Imagine you are developing an application that needs to sort data in different ways. For example, you might need to sort data in ascending order for some operations and in descending order for others. The Strategy pattern can help by defining different sorting strategies and allowing the client to choose which one to use at runtime.
Sample Python Code:
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List
# Context Class
class Context:
def __init__(self, strategy: SortingStrategy) -> None:
self._strategy = strategy
@property
def strategy(self) -> SortingStrategy:
return self._strategy
@strategy.setter
def strategy(self, strategy: SortingStrategy) -> None:
self._strategy = strategy
def do_some_business_logic(self) -> None:
print("Context: Sorting data using the strategy (not sure how it'll do it)")
result = self._strategy.sort(["e", "b", "a", "d", "c"])
print(",".join(result))
# Strategy Interface
class SortingStrategy(ABC):
@abstractmethod
def sort(self, data: List[str]) -> List[str]:
pass
# Concrete Strategies
class AscendingSortStrategy(SortingStrategy):
def sort(self, data: List[str]) -> List[str]:
return sorted(data)
class DescendingSortStrategy(SortingStrategy):
def sort(self, data: List[str]) -> List[str]:
return sorted(data, reverse=True)
# Client Code
if __name__ == "__main__":
context = Context(AscendingSortStrategy())
print("Client: Strategy is set to ascending sorting.")
context.do_some_business_logic()
print()
print("Client: Strategy is set to descending sorting.")
context.strategy = DescendingSortStrategy()
context.do_some_business_logic()
Template Method
The Template Method pattern defines the skeleton of an algorithm in an operation, deferring some steps to subclasses. It lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.
Explanation:
- Intent: Define the skeleton of an algorithm, deferring some steps to subclasses.
- Problem: Need to allow subclasses to redefine certain steps of an algorithm without changing the algorithm’s structure.
- Solution: Use a template method that calls abstract methods.
Pragmatic Python
- When: the algorithm skeleton is stable but certain steps vary; subclasses must not alter the skeleton.
- Alternative: higher-order functions or context managers instead of inheritance-based templates.
- Note: template methods enforce structure while allowing controlled variation.
Use Case: Data Processing Pipeline
Imagine you are developing a data processing pipeline where different types of data need to be processed in slightly different ways. The overall structure of the pipeline is the same, but some steps need to be customised for each data type. The Template Method pattern can help by defining the skeleton of the pipeline and allowing subclasses to customise specific steps.
Sample Python Code:
from abc import ABC, abstractmethod
# Abstract Class
class DataProcessor(ABC):
def template_method(self) -> None:
self.extract_data()
self.transform_data()
self.load_data()
self.hook1()
self.validate_data()
self.hook2()
def extract_data(self) -> None:
print("DataProcessor: Extracting data")
def transform_data(self) -> None:
print("DataProcessor: Transforming data")
def load_data(self) -> None:
print("DataProcessor: Loading data")
@abstractmethod
def validate_data(self) -> None:
pass
def hook1(self) -> None:
pass
def hook2(self) -> None:
pass
# Concrete Classes
class JSONDataProcessor(DataProcessor):
def validate_data(self) -> None:
print("JSONDataProcessor: Validating JSON data")
class CSVDataProcessor(DataProcessor):
def validate_data(self) -> None:
print("CSVDataProcessor: Validating CSV data")
def hook1(self) -> None:
print("CSVDataProcessor: Optional Hook1 for CSV data")
# Client Code
def client_code(data_processor: DataProcessor) -> None:
data_processor.template_method()
if __name__ == "__main__":
print("Client: Using JSON data processor:")
client_code(JSONDataProcessor())
print("\n")
print("Client: Using CSV data processor:")
client_code(CSVDataProcessor())
Visitor
The Visitor pattern lets you define a new operation without changing the classes of the elements on which it operates. This pattern is particularly useful for managing operations on objects of complex structures.
Explanation:
- Intent: Define a new operation without changing the classes of the elements on which it operates.
- Problem: Need to perform operations on objects without changing their classes.
- Solution: Use a visitor interface and concrete visitor classes.
Pragmatic Python
- When: the object hierarchy is fixed and you expect to add many new operations.
- Alternative:
functools.singledispatchorisinstancedispatch; use visitors when operation count grows. - Note: visitors avoid changing the core classes as operations multiply.
Use Case: File System Operations
Imagine you are developing a file system management application. The file system contains various elements like files and directories. You need to perform different operations such as calculating the total size, generating a detailed report, or checking for specific file types. The Visitor pattern can help by defining these operations as visitors that can be applied to file system elements without modifying their classes.
Sample Python Code:
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List
# Visitor Interface
class Visitor(ABC):
@abstractmethod
def visit_file(self, element: File) -> None:
pass
@abstractmethod
def visit_directory(self, element: Directory) -> None:
pass
# Concrete Visitors
class SizeVisitor(Visitor):
def visit_file(self, element: File) -> None:
print(f"File: {element.name}, Size: {element.size} KB")
def visit_directory(self, element: Directory) -> None:
print(f"Directory: {element.name}, Total Size: {element.get_total_size()} KB")
class ReportVisitor(Visitor):
def visit_file(self, element: File) -> None:
print(f"Report: File {element.name} has size {element.size} KB")
def visit_directory(self, element: Directory) -> None:
print(f"Report: Directory {element.name} contains {len(element.children)} items")
# Component Interface
class FileSystemComponent(ABC):
@abstractmethod
def accept(self, visitor: Visitor) -> None:
pass
# Concrete Components
class File(FileSystemComponent):
def __init__(self, name: str, size: int) -> None:
self.name = name
self.size = size
def accept(self, visitor: Visitor) -> None:
visitor.visit_file(self)
class Directory(FileSystemComponent):
def __init__(self, name: str) -> None:
self.name = name
self.children: List[FileSystemComponent] = []
def add(self, component: FileSystemComponent) -> None:
self.children.append(component)
def get_total_size(self) -> int:
total_size = 0
for child in self.children:
if isinstance(child, File):
total_size += child.size
elif isinstance(child, Directory):
total_size += child.get_total_size()
return total_size
def accept(self, visitor: Visitor) -> None:
visitor.visit_directory(self)
for child in self.children:
child.accept(visitor)
# Client Code
def client_code(components: List[FileSystemComponent], visitor: Visitor) -> None:
for component in components:
component.accept(visitor)
if __name__ == "__main__":
# Creating file system structure
file1 = File("file1.txt", 100)
file2 = File("file2.txt", 200)
sub_directory = Directory("subdir")
sub_directory.add(file1)
sub_directory.add(file2)
root_directory = Directory("root")
root_directory.add(sub_directory)
root_directory.add(File("file3.txt", 300))
components = [root_directory]
print("Client: Generating size report with SizeVisitor:")
size_visitor = SizeVisitor()
client_code(components, size_visitor)
print("\nClient: Generating detailed report with ReportVisitor:")
report_visitor = ReportVisitor()
client_code(components, report_visitor)
This comprehensive guide includes details, explanations, and sample Python code for each of the design patterns listed in the table of contents. In Python, prefer the smallest construct that solves the problem, and reach for a formal pattern when it clarifies intent, enables extension, or keeps responsibilities tidy as the codebase grows.