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

  1. Creational Patterns
    • Factory Method
    • Abstract Factory
    • Singleton
    • Builder
    • Prototype
  2. Structural Patterns
    • Adapter
    • Bridge
    • Composite
    • Decorator
    • Facade
    • Flyweight
    • Proxy
  3. 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 dataclass defaults/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.deepcopy or 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 @decorator syntax; 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=True dataclasses 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 for loop.
  • 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/asyncio events; 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.singledispatch or isinstance dispatch; 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.