Design Patterns in Python: The Complete Reference

Design Patterns in Python: The Complete Reference

Design patterns are a cornerstone of software engineering. They provide proven solutions to recurring design problems and enable developers to build robust, scalable, and maintainable systems. This comprehensive guide covers all 23 GoF (Gang of Four) Design Patterns, complete with theory, conceptual understanding, and Python implementations.


Table of Contents

  1. Introduction to Design Patterns

    • What Are Design Patterns?

    • Why Use Design Patterns?

    • Thinking About Design Patterns

  2. Types of Design Patterns

    • Creational Patterns

    • Structural Patterns

    • Behavioral Patterns

  3. Creational Patterns

    • Factory Method

    • Abstract Factory

    • Builder

    • Prototype

    • Singleton

  4. Structural Patterns

    • Adapter

    • Bridge

    • Composite

    • Decorator

    • Facade

    • Flyweight

    • Proxy

  5. Behavioral Patterns

    • Chain of Responsibility

    • Command

    • Interpreter

    • Iterator

    • Mediator

    • Memento

    • Observer

    • State

    • Strategy

    • Template Method

    • Visitor

  6. Real-World Applications of Patterns

  7. Summary and Best Practices


1. Introduction to Design Patterns

What Are Design Patterns?

A design pattern is a general, reusable solution to a common problem in software design. It is not a finished design that can be directly implemented but a template for solving problems in various contexts.

Why Use Design Patterns?

  • Reusability: Save time by using established solutions.

  • Maintainability: Code is easier to understand and modify.

  • Scalability: Solutions remain effective as the system grows.

  • Standardization: Shared vocabulary among developers.


Thinking About Design Patterns

Design patterns solve recurring problems by adhering to object-oriented principles such as:

  • Encapsulation

  • Inheritance

  • Polymorphism

  • Separation of concerns

Patterns are conceptual tools—apply them judiciously where they fit naturally.


2. Types of Design Patterns

1. Creational Patterns

Focus on how objects are created and instantiated, promoting flexibility in object creation.

2. Structural Patterns

Focus on object composition and relationships, ensuring that the structure is efficient and scalable.

3. Behavioral Patterns

Focus on communication and interaction between objects.


3. Creational Patterns

Factory Method

Concept

Define an interface for creating objects, but let subclasses decide which class to instantiate. Useful when the exact type of object to create isn’t known until runtime.

Theoretical Explanation

The Factory Method pattern introduces a level of abstraction in object creation, decoupling the client code from specific implementations. It adheres to the Open/Closed Principle, allowing the addition of new types without altering existing code.

Basic Implementation

from abc import ABC, abstractmethod

# Product Interface
class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass

# Concrete Products
class Dog(Animal):
    def speak(self):
        return "Woof!"

class Cat(Animal):
    def speak(self):
        return "Meow!"

# Creator
class AnimalFactory:
    @staticmethod
    def create_animal(animal_type):
        if animal_type == "dog":
            return Dog()
        elif animal_type == "cat":
            return Cat()
        else:
            return None

# Usage
animal = AnimalFactory.create_animal("dog")
print(animal.speak())  # Output: Woof!

Real-World Example: Logistics Application

from abc import ABC, abstractmethod

# Product Interface
class Transport(ABC):
    @abstractmethod
    def deliver(self):
        pass

# Concrete Products
class Truck(Transport):
    def deliver(self):
        return "Delivering by land in a truck."

class Ship(Transport):
    def deliver(self):
        return "Delivering by sea in a ship."

# Creator Interface
class Logistics(ABC):
    @abstractmethod
    def create_transport(self):
        pass

    def plan_delivery(self):
        transport = self.create_transport()
        return transport.deliver()

# Concrete Creators
class RoadLogistics(Logistics):
    def create_transport(self):
        return Truck()

class SeaLogistics(Logistics):
    def create_transport(self):
        return Ship()

# Usage
logistics = RoadLogistics()
print(logistics.plan_delivery())  # Output: Delivering by land in a truck.

logistics = SeaLogistics()
print(logistics.plan_delivery())  # Output: Delivering by sea in a ship.

Abstract Factory

Concept

Provide an interface for creating families of related or dependent objects without specifying their concrete classes.

Theoretical Explanation

The Abstract Factory pattern creates a higher level of abstraction by grouping related factory methods. It ensures compatibility between related products, making it especially useful for systems that require cross-compatible objects.

Basic Implementation

from abc import ABC, abstractmethod

# Abstract Products
class Button(ABC):
    @abstractmethod
    def render(self):
        pass

class Checkbox(ABC):
    @abstractmethod
    def render(self):
        pass

# Concrete Products
class WindowsButton(Button):
    def render(self):
        return "Windows Button"

class MacOSButton(Button):
    def render(self):
        return "MacOS Button"

# Abstract Factory
class GUIFactory(ABC):
    @abstractmethod
    def create_button(self):
        pass

    @abstractmethod
    def create_checkbox(self):
        pass

# Concrete Factories
class WindowsFactory(GUIFactory):
    def create_button(self):
        return WindowsButton()

    def create_checkbox(self):
        return "Windows Checkbox"

class MacOSFactory(GUIFactory):
    def create_button(self):
        return MacOSButton()

    def create_checkbox(self):
        return "MacOS Checkbox"

# Usage
factory = WindowsFactory()
print(factory.create_button().render())  # Output: Windows Button
print(factory.create_checkbox())         # Output: Windows Checkbox

Real-World Example: GUI Framework

class WindowsCheckbox(Checkbox):
    def render(self):
        return "Rendering Windows Checkbox."

class MacOSCheckbox(Checkbox):
    def render(self):
        return "Rendering MacOS Checkbox."

# Usage
factory = MacOSFactory()
button = factory.create_button()
checkbox = factory.create_checkbox()
print(button.render())   # Output: Rendering MacOS Button.
print(checkbox.render()) # Output: Rendering MacOS Checkbox.

Builder

Concept

Separate the construction of a complex object from its representation so that the same construction process can create different representations.

Theoretical Explanation

The Builder pattern is particularly useful for creating complex objects with many optional parameters or configurations. It encapsulates the construction logic, adhering to the Single Responsibility Principle.

Basic Implementation

# Product
class House:
    def __init__(self):
        self.floor = None
        self.walls = None
        self.roof = None

    def __str__(self):
        return f"House with {self.floor}, {self.walls}, and {self.roof}."

# Builder Interface
class HouseBuilder:
    def build_floor(self, floor_type):
        pass

    def build_walls(self, wall_type):
        pass

    def build_roof(self, roof_type):
        pass

# Concrete Builder
class ConcreteHouseBuilder(HouseBuilder):
    def __init__(self):
        self.house = House()

    def build_floor(self):
        self.house.floor = "Concrete Floor"

    def build_walls(self):
        self.house.walls = "Brick Walls"

    def build_roof(self):
        self.house.roof = "Metal Roof"

    def get_house(self):
        return self.house

# Director
class Director:
    def __init__(self, builder):
        self.builder = builder

    def construct_house(self):
        self.builder.build_floor()
        self.builder.build_walls()
        self.builder.build_roof()

# Usage
builder = ConcreteHouseBuilder()
director = Director(builder)
director.construct_house()
house = builder.get_house()
print(house)
# Output: House with Concrete Floor, Brick Walls, and Metal Roof.

Prototype

Concept

The Prototype pattern is used to create new objects by copying an existing object (prototype) instead of creating from scratch. It’s particularly useful when object creation is costly or complex.

Theoretical Explanation

  • A prototype instance is used as a blueprint for creating new objects.

  • Provides an alternative to constructors.

  • Useful for scenarios involving deep copying or custom initialization.

Basic Implementation

import copy

class Prototype:
    def __init__(self):
        self._objects = {}

    def register_object(self, name, obj):
        self._objects[name] = obj

    def unregister_object(self, name):
        del self._objects[name]

    def clone(self, name, **attributes):
        obj = copy.deepcopy(self._objects.get(name))
        obj.__dict__.update(attributes)
        return obj

# Usage
class Car:
    def __init__(self, model, color):
        self.model = model
        self.color = color

    def __str__(self):
        return f"{self.color} {self.model}"

prototype = Prototype()
car = Car("Sedan", "Red")
prototype.register_object("base_car", car)

new_car = prototype.clone("base_car", color="Blue")
print(new_car)  # Output: Blue Sedan

Real-World Example: Graphic Editor

Scenario: A graphic design app that duplicates shapes.

class Shape:
    def __init__(self, x, y, color):
        self.x = x
        self.y = y
        self.color = color

    def clone(self):
        return copy.deepcopy(self)

    def __str__(self):
        return f"Shape({self.x}, {self.y}, {self.color})"

# Usage
circle = Shape(10, 20, "Red")
circle_clone = circle.clone()
circle_clone.color = "Blue"

print(circle)       # Output: Shape(10, 20, Red)
print(circle_clone) # Output: Shape(10, 20, Blue)

Singleton

Concept

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it.

Theoretical Explanation

  • Enforces a single instance across the system.

  • Useful for managing shared resources (e.g., database connections, logging).

Basic Implementation

class SingletonMeta(type):
    _instances = {}

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

class DatabaseConnection(metaclass=SingletonMeta):
    def connect(self):
        return "Database connected."

# Usage
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2)  # Output: True

Real-World Example: Logger

Scenario: A logging service where multiple modules write to the same log.

class Logger(metaclass=SingletonMeta):
    def __init__(self):
        self.log_file = "app.log"

    def write_log(self, message):
        print(f"Writing to {self.log_file}: {message}")

# Usage
logger1 = Logger()
logger2 = Logger()

logger1.write_log("Starting application...")
logger2.write_log("Application error.")

print(logger1 is logger2)  # Output: True

4. Structural Patterns


Adapter

Concept

The Adapter pattern allows incompatible interfaces to work together by wrapping an existing class with a new interface.

Theoretical Explanation

  • Converts one interface into another expected by the client.

  • Useful when integrating third-party libraries or legacy code.

Basic Implementation

class EuropeanSocket:
    def provide_electricity(self):
        return "230V AC"

class Adapter:
    def __init__(self, socket):
        self.socket = socket

    def provide_110v(self):
        return "Converting 230V to 110V."

# Usage
european_socket = EuropeanSocket()
adapter = Adapter(european_socket)
print(adapter.provide_110v())  # Output: Converting 230V to 110V.

Real-World Example: Payment Gateway

Scenario: An e-commerce platform supporting multiple payment methods.

class Stripe:
    def make_payment(self, amount):
        return f"Paid {amount} using Stripe."

class PayPal:
    def send_money(self, amount):
        return f"Paid {amount} using PayPal."

class PaymentAdapter:
    def __init__(self, payment_system):
        self.payment_system = payment_system

    def pay(self, amount):
        if isinstance(self.payment_system, Stripe):
            return self.payment_system.make_payment(amount)
        elif isinstance(self.payment_system, PayPal):
            return self.payment_system.send_money(amount)

# Usage
stripe = PaymentAdapter(Stripe())
paypal = PaymentAdapter(PayPal())

print(stripe.pay(100))  # Output: Paid 100 using Stripe.
print(paypal.pay(200))  # Output: Paid 200 using PayPal.

Bridge

Concept

The Bridge pattern decouples an abstraction from its implementation, allowing the two to vary independently.

Theoretical Explanation

  • Separates what an object does from how it does it.

  • Useful when implementations can change dynamically.

Basic Implementation

from abc import ABC, abstractmethod

class DrawingAPI(ABC):
    @abstractmethod
    def draw_circle(self, x, y, radius):
        pass

class VectorAPI(DrawingAPI):
    def draw_circle(self, x, y, radius):
        return f"VectorAPI: Drawing circle at ({x}, {y}) with radius {radius}."

class RasterAPI(DrawingAPI):
    def draw_circle(self, x, y, radius):
        return f"RasterAPI: Drawing circle at ({x}, {y}) with radius {radius}."

class Shape:
    def __init__(self, drawing_api):
        self.drawing_api = drawing_api

class Circle(Shape):
    def __init__(self, x, y, radius, drawing_api):
        super().__init__(drawing_api)
        self.x = x
        self.y = y
        self.radius = radius

    def draw(self):
        return self.drawing_api.draw_circle(self.x, self.y, self.radius)

# Usage
circle1 = Circle(5, 10, 15, VectorAPI())
circle2 = Circle(7, 14, 21, RasterAPI())

print(circle1.draw())  # Output: VectorAPI: Drawing circle at (5, 10) with radius 15.
print(circle2.draw())  # Output: RasterAPI: Drawing circle at (7, 14) with radius 21.

Composite

Concept

The Composite pattern allows you to treat individual objects and compositions of objects uniformly. It represents part-whole hierarchies, making it easy to work with both simple and complex structures.

Theoretical Explanation

  • Simplifies handling tree-like structures (e.g., file systems, organization charts).

  • Composite objects contain both leaf and composite children.

Basic Implementation

from abc import ABC, abstractmethod

class Component(ABC):
    @abstractmethod
    def operation(self):
        pass

class Leaf(Component):
    def __init__(self, name):
        self.name = name

    def operation(self):
        return f"Leaf: {self.name}"

class Composite(Component):
    def __init__(self, name):
        self.name = name
        self.children = []

    def add(self, component):
        self.children.append(component)

    def operation(self):
        results = [child.operation() for child in self.children]
        return f"Composite: {self.name} containing [{', '.join(results)}]"

# Usage
leaf1 = Leaf("Leaf1")
leaf2 = Leaf("Leaf2")
composite = Composite("Composite1")
composite.add(leaf1)
composite.add(leaf2)

print(composite.operation())
# Output: Composite: Composite1 containing [Leaf: Leaf1, Leaf: Leaf2]

Real-World Example: File System

Scenario: A directory can contain files or other directories.

class FileSystemComponent(ABC):
    @abstractmethod
    def show_details(self, indent=0):
        pass

class File(FileSystemComponent):
    def __init__(self, name, size):
        self.name = name
        self.size = size

    def show_details(self, indent=0):
        return f"{' ' * indent}File: {self.name} ({self.size} KB)"

class Directory(FileSystemComponent):
    def __init__(self, name):
        self.name = name
        self.children = []

    def add(self, component):
        self.children.append(component)

    def show_details(self, indent=0):
        details = f"{' ' * indent}Directory: {self.name}\n"
        for child in self.children:
            details += child.show_details(indent + 2) + "\n"
        return details.strip()

# Usage
root = Directory("root")
sub_dir = Directory("sub_dir")
root.add(File("file1.txt", 100))
root.add(sub_dir)
sub_dir.add(File("file2.txt", 200))
sub_dir.add(File("file3.txt", 300))

print(root.show_details())
# Output:
# Directory: root
#   File: file1.txt (100 KB)
#   Directory: sub_dir
#     File: file2.txt (200 KB)
#     File: file3.txt (300 KB)

Decorator

Concept

The Decorator pattern adds responsibilities to objects dynamically, providing a flexible alternative to subclassing for extending functionality.

Theoretical Explanation

  • Allows behavior to be added to individual objects without affecting others.

  • Adheres to the Open/Closed Principle: Open for extension but closed for modification.

Basic Implementation

class Component(ABC):
    @abstractmethod
    def operation(self):
        pass

class ConcreteComponent(Component):
    def operation(self):
        return "ConcreteComponent"

class Decorator(Component):
    def __init__(self, component):
        self.component = component

    def operation(self):
        return self.component.operation()

class ConcreteDecoratorA(Decorator):
    def operation(self):
        return f"ConcreteDecoratorA({self.component.operation()})"

class ConcreteDecoratorB(Decorator):
    def operation(self):
        return f"ConcreteDecoratorB({self.component.operation()})"

# Usage
component = ConcreteComponent()
decorated = ConcreteDecoratorA(ConcreteDecoratorB(component))
print(decorated.operation())
# Output: ConcreteDecoratorA(ConcreteDecoratorB(ConcreteComponent))

Real-World Example: Pizza Toppings

Scenario: Dynamically add toppings to a pizza.

class Pizza:
    def cost(self):
        return 10

    def description(self):
        return "Plain Pizza"

class ToppingDecorator(Pizza):
    def __init__(self, pizza):
        self.pizza = pizza

    def cost(self):
        return self.pizza.cost()

    def description(self):
        return self.pizza.description()

class Cheese(ToppingDecorator):
    def cost(self):
        return self.pizza.cost() + 2

    def description(self):
        return self.pizza.description() + ", Cheese"

class Olives(ToppingDecorator):
    def cost(self):
        return self.pizza.cost() + 1.5

    def description(self):
        return self.pizza.description() + ", Olives"

# Usage
pizza = Pizza()
pizza = Cheese(pizza)
pizza = Olives(pizza)

print(pizza.description(), "Cost:", pizza.cost())
# Output: Plain Pizza, Cheese, Olives Cost: 13.5

Facade

Concept

The Facade pattern provides a simplified interface to a complex subsystem, making it easier to use.

Theoretical Explanation

  • Hides the complexity of the system from the client.

  • Encapsulates a set of interfaces into a single higher-level interface.

Basic Implementation

class SubsystemA:
    def operation_a(self):
        return "SubsystemA: Ready!"

class SubsystemB:
    def operation_b(self):
        return "SubsystemB: Go!"

class Facade:
    def __init__(self):
        self.subsystem_a = SubsystemA()
        self.subsystem_b = SubsystemB()

    def operation(self):
        return f"{self.subsystem_a.operation_a()} + {self.subsystem_b.operation_b()}"

# Usage
facade = Facade()
print(facade.operation())
# Output: SubsystemA: Ready! + SubsystemB: Go!

Real-World Example: Video Conversion

Scenario: Simplify video conversion that involves multiple subsystems.

class VideoFile:
    def __init__(self, filename):
        self.filename = filename

class AudioMixer:
    def fix(self, filename):
        return f"Audio fixed for {filename}."

class VideoEditor:
    def crop(self, filename):
        return f"Video cropped for {filename}."

class CodecConverter:
    def convert(self, filename, format):
        return f"{filename} converted to {format} format."

class VideoConverterFacade:
    def convert_video(self, filename, format):
        audio_mixer = AudioMixer()
        video_editor = VideoEditor()
        codec_converter = CodecConverter()

        audio = audio_mixer.fix(filename)
        video = video_editor.crop(filename)
        conversion = codec_converter.convert(filename, format)

        return f"{audio}\n{video}\n{conversion}"

# Usage
converter = VideoConverterFacade()
result = converter.convert_video("movie.mp4", "AVI")
print(result)
# Output:
# Audio fixed for movie.mp4.
# Video cropped for movie.mp4.
# movie.mp4 converted to AVI format.

Flyweight

Concept

The Flyweight pattern minimizes memory usage by sharing as much data as possible with similar objects.

Theoretical Explanation

  • Reduces memory usage by sharing immutable data.

  • Useful for applications where many objects are created (e.g., graphical apps).

Basic Implementation

class Flyweight:
    def __init__(self, shared_state):
        self.shared_state = shared_state

    def operation(self, unique_state):
        return f"Flyweight: Shared({self.shared_state}), Unique({unique_state})"

class FlyweightFactory:
    _flyweights = {}

    @staticmethod
    def get_flyweight(shared_state):
        if shared_state not in FlyweightFactory._flyweights:
            FlyweightFactory._flyweights[shared_state] = Flyweight(shared_state)
        return FlyweightFactory._flyweights[shared_state]

# Usage
factory = FlyweightFactory()
flyweight1 = factory.get_flyweight("Shared1")
flyweight2 = factory.get_flyweight("Shared1")

print(flyweight1.operation("Unique1"))
print(flyweight2.operation("Unique2"))
# Output:
# Flyweight: Shared(Shared1), Unique(Unique1)
# Flyweight: Shared(Shared1), Unique(Unique2)

This covers Composite, Decorator, Facade, and Flyweight. I’ll proceed with Proxy and then move to Behavioral Patterns. Let me know if you’d like adjustments!

Proxy

Concept

The Proxy pattern provides a surrogate or placeholder for another object to control access to it. Proxies are often used to defer resource-intensive operations, add security layers, or perform logging.

Theoretical Explanation

  • Proxies act as intermediaries, controlling access to a target object.

  • Types of proxies include:

    • Virtual Proxy: Delays creation or initialization.

    • Protection Proxy: Adds access control.

    • Logging Proxy: Tracks interactions.


Basic Implementation

from abc import ABC, abstractmethod

class Subject(ABC):
    @abstractmethod
    def request(self):
        pass

class RealSubject(Subject):
    def request(self):
        return "RealSubject: Handling request."

class Proxy(Subject):
    def __init__(self, real_subject):
        self.real_subject = real_subject

    def request(self):
        print("Proxy: Logging access to the real subject.")
        return self.real_subject.request()

# Usage
real_subject = RealSubject()
proxy = Proxy(real_subject)

print(proxy.request())
# Output:
# Proxy: Logging access to the real subject.
# RealSubject: Handling request.

Real-World Example: Virtual Proxy

Scenario: An image viewer application that delays loading high-resolution images until needed.

class HighResolutionImage:
    def __init__(self, filename):
        self.filename = filename
        self.load_image_from_disk()

    def load_image_from_disk(self):
        print(f"Loading high-resolution image: {self.filename}")

    def display(self):
        print(f"Displaying high-resolution image: {self.filename}")

class ImageProxy:
    def __init__(self, filename):
        self.filename = filename
        self.real_image = None

    def display(self):
        if not self.real_image:
            self.real_image = HighResolutionImage(self.filename)
        self.real_image.display()

# Usage
image = ImageProxy("large_photo.jpg")
print("Image proxy created. High-resolution image not loaded yet.")

image.display()  # Image is loaded and displayed now.
image.display()  # Image is displayed again without loading.

# Output:
# Image proxy created. High-resolution image not loaded yet.
# Loading high-resolution image: large_photo.jpg
# Displaying high-resolution image: large_photo.jpg
# Displaying high-resolution image: large_photo.jpg

5. Behavioral Patterns


Chain of Responsibility

Concept

The Chain of Responsibility pattern allows multiple objects to handle a request without coupling the sender to the receiver. Each object in the chain can either handle the request or pass it to the next object.

Theoretical Explanation

  • Decouples the sender of a request from its receivers.

  • Avoids hard-coding handlers into the request sender.

  • Particularly useful for handling logging, UI events, or validation workflows.


Basic Implementation

from abc import ABC, abstractmethod

class Handler(ABC):
    def __init__(self, next_handler=None):
        self.next_handler = next_handler

    @abstractmethod
    def handle_request(self, request):
        pass

class ConcreteHandler1(Handler):
    def handle_request(self, request):
        if 0 < request <= 10:
            return f"Handler1 handled request {request}."
        elif self.next_handler:
            return self.next_handler.handle_request(request)

class ConcreteHandler2(Handler):
    def handle_request(self, request):
        if 10 < request <= 20:
            return f"Handler2 handled request {request}."
        elif self.next_handler:
            return self.next_handler.handle_request(request)

class DefaultHandler(Handler):
    def handle_request(self, request):
        return f"No handler could process request {request}."

# Usage
handler_chain = ConcreteHandler1(ConcreteHandler2(DefaultHandler()))
print(handler_chain.handle_request(5))    # Output: Handler1 handled request 5.
print(handler_chain.handle_request(15))   # Output: Handler2 handled request 15.
print(handler_chain.handle_request(25))   # Output: No handler could process request 25.

Real-World Example: Customer Support System

Scenario: A customer support system where requests are handled at different levels (basic, advanced, supervisor).

class SupportHandler(ABC):
    def __init__(self, next_handler=None):
        self.next_handler = next_handler

    @abstractmethod
    def handle_request(self, request):
        pass

class BasicSupport(SupportHandler):
    def handle_request(self, request):
        if request == "basic":
            return "BasicSupport: Handled basic request."
        elif self.next_handler:
            return self.next_handler.handle_request(request)

class AdvancedSupport(SupportHandler):
    def handle_request(self, request):
        if request == "advanced":
            return "AdvancedSupport: Handled advanced request."
        elif self.next_handler:
            return self.next_handler.handle_request(request)

class SupervisorSupport(SupportHandler):
    def handle_request(self, request):
        return f"SupervisorSupport: Escalated request - {request}."

# Usage
support_chain = BasicSupport(AdvancedSupport(SupervisorSupport()))
print(support_chain.handle_request("basic"))       # Output: BasicSupport: Handled basic request.
print(support_chain.handle_request("advanced"))    # Output: AdvancedSupport: Handled advanced request.
print(support_chain.handle_request("critical"))    # Output: SupervisorSupport: Escalated request - critical.

Command

Concept

The Command pattern encapsulates a request as an object, allowing parameterization, queuing, and logging of requests.

Theoretical Explanation

  • Encapsulates actions or operations as objects.

  • Decouples the sender from the receiver.

  • Commonly used for undo/redo functionality.


Basic Implementation

class Command(ABC):
    @abstractmethod
    def execute(self):
        pass

class Receiver:
    def action(self):
        return "Receiver: Executing action."

class ConcreteCommand(Command):
    def __init__(self, receiver):
        self.receiver = receiver

    def execute(self):
        return self.receiver.action()

class Invoker:
    def set_command(self, command):
        self.command = command

    def execute_command(self):
        return self.command.execute()

# Usage
receiver = Receiver()
command = ConcreteCommand(receiver)
invoker = Invoker()
invoker.set_command(command)

print(invoker.execute_command())
# Output: Receiver: Executing action.

Real-World Example: Smart Home System

Scenario: A smart home system controlling lights and devices.

class Light:
    def turn_on(self):
        return "Light turned ON."

    def turn_off(self):
        return "Light turned OFF."

class TurnOnLightCommand(Command):
    def __init__(self, light):
        self.light = light

    def execute(self):
        return self.light.turn_on()

class TurnOffLightCommand(Command):
    def __init__(self, light):
        self.light = light

    def execute(self):
        return self.light.turn_off()

class RemoteControl:
    def __init__(self):
        self.commands = []

    def set_command(self, command):
        self.commands.append(command)

    def press_button(self):
        return [command.execute() for command in self.commands]

# Usage
light = Light()
remote = RemoteControl()
remote.set_command(TurnOnLightCommand(light))
remote.set_command(TurnOffLightCommand(light))

print(remote.press_button())
# Output:
# ['Light turned ON.', 'Light turned OFF.']

Interpreter

Concept

The Interpreter pattern defines a grammar for a language and an interpreter that parses and evaluates sentences in that language. It’s useful for creating scripting languages or expression evaluators.

Theoretical Explanation

  • Represents grammar rules as classes.

  • Each grammar rule has an interpret method for evaluation.

  • Useful for building mathematical expression evaluators, parsers, or query engines.


Basic Implementation

class Expression:
    def interpret(self):
        pass

class Number(Expression):
    def __init__(self, value):
        self.value = value

    def interpret(self):
        return self.value

class Add(Expression):
    def __init__(self, left, right):
        self.left = left
        self.right = right

    def interpret(self):
        return self.left.interpret() + self.right.interpret()

class Subtract(Expression):
    def __init__(self, left, right):
        self.left = left
        self.right = right

    def interpret(self):
        return self.left.interpret() - self.right.interpret()

# Usage
expression = Add(Number(10), Subtract(Number(20), Number(5)))
print(expression.interpret())  # Output: 25

Real-World Example: Calculator for Expressions

Scenario: A calculator for simple mathematical expressions.

class Context:
    def __init__(self):
        self.variables = {}

    def set_variable(self, name, value):
        self.variables[name] = value

    def get_variable(self, name):
        return self.variables.get(name, 0)

class Variable(Expression):
    def __init__(self, name):
        self.name = name

    def interpret(self, context):
        return context.get_variable(self.name)

# Usage
context = Context()
context.set_variable("x", 10)
context.set_variable("y", 5)

expression = Add(Variable("x"), Subtract(Number(20), Variable("y")))
print(expression.interpret(context))  # Output: 25

Iterator

Concept

The Iterator pattern provides a way to access elements of a collection sequentially without exposing its underlying representation.

Theoretical Explanation

  • Encapsulates the iteration logic.

  • Decouples iteration from the collection, adhering to the Single Responsibility Principle.

  • Commonly used in Python (for loops inherently use iterators).


Basic Implementation

class NumberIterator:
    def __init__(self, numbers):
        self.numbers = numbers
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.index < len(self.numbers):
            value = self.numbers[self.index]
            self.index += 1
            return value
        else:
            raise StopIteration

# Usage
numbers = NumberIterator([1, 2, 3, 4])
for num in numbers:
    print(num)

# Output:
# 1
# 2
# 3
# 4

Real-World Example: Playlist Iterator

Scenario: A music playlist iterates over songs.

class Song:
    def __init__(self, title, artist):
        self.title = title
        self.artist = artist

    def __str__(self):
        return f"{self.title} by {self.artist}"

class Playlist:
    def __init__(self):
        self.songs = []

    def add_song(self, song):
        self.songs.append(song)

    def __iter__(self):
        return iter(self.songs)

# Usage
playlist = Playlist()
playlist.add_song(Song("Song A", "Artist 1"))
playlist.add_song(Song("Song B", "Artist 2"))

for song in playlist:
    print(song)

# Output:
# Song A by Artist 1
# Song B by Artist 2

Mediator

Concept

The Mediator pattern defines an object that encapsulates how a set of objects interact. It promotes loose coupling by preventing objects from referring to each other explicitly.

Theoretical Explanation

  • Simplifies communication between objects by introducing a mediator.

  • Adheres to the Single Responsibility Principle by centralizing communication logic.


Basic Implementation

class Mediator:
    def notify(self, sender, event):
        pass

class ConcreteMediator(Mediator):
    def __init__(self, component1, component2):
        self.component1 = component1
        self.component2 = component2

    def notify(self, sender, event):
        if event == "A":
            return f"Mediator reacts to A and triggers B."
        elif event == "B":
            return f"Mediator reacts to B and triggers A."

class Component1:
    def __init__(self, mediator):
        self.mediator = mediator

    def do_a(self):
        return self.mediator.notify(self, "A")

class Component2:
    def __init__(self, mediator):
        self.mediator = mediator

    def do_b(self):
        return self.mediator.notify(self, "B")

# Usage
mediator = ConcreteMediator(Component1, Component2)
component1 = Component1(mediator)
component2 = Component2(mediator)

print(component1.do_a())  # Output: Mediator reacts to A and triggers B.
print(component2.do_b())  # Output: Mediator reacts to B and triggers A.

Real-World Example: Chat Room

Scenario: A chat room where users interact through a mediator.

class ChatRoom:
    def show_message(self, user, message):
        print(f"[{user.name}]: {message}")

class User:
    def __init__(self, name, chat_room):
        self.name = name
        self.chat_room = chat_room

    def send(self, message):
        self.chat_room.show_message(self, message)

# Usage
chat_room = ChatRoom()
user1 = User("Alice", chat_room)
user2 = User("Bob", chat_room)

user1.send("Hello, Bob!")
user2.send("Hi, Alice!")

# Output:
# [Alice]: Hello, Bob!
# [Bob]: Hi, Alice!

Memento

Concept

The Memento pattern captures and externalizes an object’s internal state without violating encapsulation. It allows state restoration later.

Theoretical Explanation

  • Useful for implementing undo/redo functionality.

  • Adheres to the Single Responsibility Principle by separating state storage from other logic.


Basic Implementation

class Memento:
    def __init__(self, state):
        self.state = state

class Originator:
    def __init__(self):
        self._state = ""

    def set_state(self, state):
        self._state = state

    def save(self):
        return Memento(self._state)

    def restore(self, memento):
        self._state = memento.state

# Usage
originator = Originator()
originator.set_state("State1")
memento = originator.save()

originator.set_state("State2")
print(originator._state)  # Output: State2

originator.restore(memento)
print(originator._state)  # Output: State1

Real-World Example: Text Editor

Scenario: A text editor with undo functionality.

class EditorState:
    def __init__(self, content):
        self.content = content

class Editor:
    def __init__(self):
        self.content = ""

    def type(self, words):
        self.content += words

    def save(self):
        return EditorState(self.content)

    def restore(self, state):
        self.content = state.content

# Usage
editor = Editor()
editor.type("Hello, ")
state = editor.save()

editor.type("world!")
print(editor.content)  # Output: Hello, world!

editor.restore(state)
print(editor.content)  # Output: Hello,

Observer

Concept

The Observer pattern establishes a one-to-many dependency between objects, ensuring that when one object changes state, all its dependents are notified and updated automatically.

Theoretical Explanation

  • Promotes loose coupling between the subject and observers.

  • Useful in event-driven systems, where multiple components react to a single event.


Basic Implementation

class Subject:
    def __init__(self):
        self._observers = []

    def add_observer(self, observer):
        self._observers.append(observer)

    def notify_observers(self):
        for observer in self._observers:
            observer.update(self)

class Observer:
    def update(self, subject):
        pass

class ConcreteObserver(Observer):
    def update(self, subject):
        print("Observer notified!")

# Usage
subject = Subject()
observer1 = ConcreteObserver()
observer2 = ConcreteObserver()

subject.add_observer(observer1)
subject.add_observer(observer2)

subject.notify_observers()
# Output:
# Observer notified!
# Observer notified!

Real-World Example: Stock Price Tracker

Scenario: A stock price tracker notifies investors of changes in stock prices.

class Stock:
    def __init__(self, name):
        self.name = name
        self.price = 0
        self._observers = []

    def add_observer(self, observer):
        self._observers.append(observer)

    def set_price(self, price):
        self.price = price
        self.notify_observers()

    def notify_observers(self):
        for observer in self._observers:
            observer.update(self)

class Investor:
    def update(self, stock):
        print(f"Investor notified: {stock.name} price changed to {stock.price}.")

# Usage
apple_stock = Stock("Apple")
investor1 = Investor()
investor2 = Investor()

apple_stock.add_observer(investor1)
apple_stock.add_observer(investor2)

apple_stock.set_price(150)
# Output:
# Investor notified: Apple price changed to 150.
# Investor notified: Apple price changed to 150.

State

Concept

The State pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class.

Theoretical Explanation

  • Encapsulates state-specific behavior and transitions into separate classes.

  • Eliminates complex conditionals in the object.


Basic Implementation

class State:
    def handle(self, context):
        pass

class ConcreteStateA(State):
    def handle(self, context):
        print("State A handling request. Switching to State B.")
        context.state = ConcreteStateB()

class ConcreteStateB(State):
    def handle(self, context):
        print("State B handling request. Switching to State A.")
        context.state = ConcreteStateA()

class Context:
    def __init__(self):
        self.state = ConcreteStateA()

    def request(self):
        self.state.handle(self)

# Usage
context = Context()
context.request()  # Output: State A handling request. Switching to State B.
context.request()  # Output: State B handling request. Switching to State A.

Real-World Example: Document Workflow

Scenario: A document goes through different states: Draft, Moderation, Published.

class DocumentState:
    def next_state(self, document):
        pass

    def get_status(self):
        pass

class DraftState(DocumentState):
    def next_state(self, document):
        document.state = ModerationState()

    def get_status(self):
        return "Draft"

class ModerationState(DocumentState):
    def next_state(self, document):
        document.state = PublishedState()

    def get_status(self):
        return "Under Moderation"

class PublishedState(DocumentState):
    def next_state(self, document):
        print("Document is already published!")

    def get_status(self):
        return "Published"

class Document:
    def __init__(self):
        self.state = DraftState()

    def next_state(self):
        self.state.next_state(self)

    def get_status(self):
        return self.state.get_status()

# Usage
doc = Document()
print(doc.get_status())  # Output: Draft
doc.next_state()
print(doc.get_status())  # Output: Under Moderation
doc.next_state()
print(doc.get_status())  # Output: Published

Strategy

Concept

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.

Theoretical Explanation

  • Encapsulates behaviors or algorithms into separate classes.

  • Adheres to the Open/Closed Principle by allowing algorithms to be added without modifying the context.


Basic Implementation

class Strategy:
    def execute(self):
        pass

class ConcreteStrategyA(Strategy):
    def execute(self):
        return "Strategy A executed."

class ConcreteStrategyB(Strategy):
    def execute(self):
        return "Strategy B executed."

class Context:
    def __init__(self, strategy):
        self.strategy = strategy

    def set_strategy(self, strategy):
        self.strategy = strategy

    def execute_strategy(self):
        return self.strategy.execute()

# Usage
context = Context(ConcreteStrategyA())
print(context.execute_strategy())  # Output: Strategy A executed.

context.set_strategy(ConcreteStrategyB())
print(context.execute_strategy())  # Output: Strategy B executed.

Real-World Example: Payment System

Scenario: A payment system supports multiple payment methods (e.g., PayPal, credit card).

class PaymentStrategy:
    def pay(self, amount):
        pass

class PayPalStrategy(PaymentStrategy):
    def pay(self, amount):
        return f"Paid {amount} using PayPal."

class CreditCardStrategy(PaymentStrategy):
    def pay(self, amount):
        return f"Paid {amount} using Credit Card."

class PaymentContext:
    def __init__(self, strategy):
        self.strategy = strategy

    def set_strategy(self, strategy):
        self.strategy = strategy

    def execute_payment(self, amount):
        return self.strategy.pay(amount)

# Usage
paypal = PayPalStrategy()
credit_card = CreditCardStrategy()

context = PaymentContext(paypal)
print(context.execute_payment(100))  # Output: Paid 100 using PayPal.

context.set_strategy(credit_card)
print(context.execute_payment(200))  # Output: Paid 200 using Credit Card.

Template Method

Concept

The Template Method pattern defines the skeleton of an algorithm in an operation, deferring some steps to subclasses.

Theoretical Explanation

  • Allows specific steps of an algorithm to be overridden without altering the algorithm's structure.

  • Promotes reuse by extracting common behavior into a template.


Basic Implementation

class AbstractClass:
    def template_method(self):
        self.step_one()
        self.step_two()
        self.hook()

    def step_one(self):
        print("AbstractClass: Step one.")

    def step_two(self):
        pass

    def hook(self):
        pass

class ConcreteClass(AbstractClass):
    def step_two(self):
        print("ConcreteClass: Step two.")

# Usage
concrete = ConcreteClass()
concrete.template_method()
# Output:
# AbstractClass: Step one.
# ConcreteClass: Step two.

Real-World Example: Data Analysis Pipeline

Scenario: A data analysis pipeline with common preprocessing steps.

class DataPipeline:
    def run_pipeline(self):
        self.load_data()
        self.clean_data()
        self.analyze_data()
        self.visualize_results()

    def load_data(self):
        pass

    def clean_data(self):
        print("Cleaning data...")

    def analyze_data(self):
        print("Analyzing data...")

    def visualize_results(self):
        print("Visualizing results...")

class CSVDataPipeline(DataPipeline):
    def load_data(self):
        print("Loading data from CSV.")

class JSONDataPipeline(DataPipeline):
    def load_data(self):
        print("Loading data from JSON.")

# Usage
csv_pipeline = CSVDataPipeline()
csv_pipeline.run_pipeline()
# Output:
# Loading data from CSV.
# Cleaning data...
# Analyzing data...
# Visualizing results...

json_pipeline = JSONDataPipeline()
json_pipeline.run_pipeline()
# Output:
# Loading data from JSON.
# Cleaning data...
# Analyzing data...
# Visualizing results...

Visitor

Concept

The Visitor pattern separates algorithms from the objects on which they operate, allowing new operations to be added without modifying existing classes.

Theoretical Explanation

  • Adheres to the Single Responsibility Principle by separating operations from object structures.

  • Allows adding new operations without modifying the object structure.


Basic Implementation

class Visitor:
    def visit(self, element):
        pass

class ConcreteVisitor(Visitor):
    def visit(self, element):
        print(f"Visited {element.name}")

class Element:
    def accept(self, visitor):
        pass

class ConcreteElement(Element):
    def __init__(self, name):
        self.name = name

    def accept(self, visitor):
        visitor.visit(self)

# Usage
visitor = ConcreteVisitor()
element = ConcreteElement("Element1")
element.accept(visitor)
# Output: Visited Element1

Real-World Example: Shopping Cart Discounts

Scenario: Apply different discounts to product categories.

class DiscountVisitor:
    def visit_electronics(self, item):
        return f"10% off on {item.name}: {item.price * 0.9}"

    def visit_clothing(self, item):
        return f"20% off on {item.name}: {item.price * 0.8}"

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def accept(self, visitor):
        pass

class Electronics(Product):
    def accept(self, visitor):
        return visitor.visit_electronics(self)

class Clothing(Product):
    def accept(self, visitor):
        return visitor.visit_clothing(self)

# Usage
visitor = DiscountVisitor()
tv = Electronics("TV", 1000)
shirt = Clothing("Shirt", 50)

print(tv.accept(visitor))   # Output: 10% off on TV: 900.0
print(shirt.accept(visitor)) # Output: 20% off on Shirt: 40.0

6. Real-World Applications of Patterns

Design patterns are widely used across industries to address common challenges in software development. Here’s how these patterns shine in various domains:

1. Web Development

  • Singleton: Managing database connections or a global configuration object in frameworks like Django or Flask.

  • Factory Method: Creating dynamic components such as form fields or widgets based on user input or database schema.

  • Template Method: Standardizing workflows like request validation, processing, and response in REST APIs.

2. Game Development

  • Observer: Tracking player events, such as health updates, inventory changes, or quest progression.

  • Flyweight: Optimizing memory usage for rendering a large number of game objects (e.g., trees, enemies).

  • State: Managing player states such as walking, running, or attacking.

3. E-Commerce

  • Strategy: Implementing dynamic pricing strategies, such as discounts, taxes, or shipping calculations.

  • Command: Managing user actions like adding items to a cart, processing payments, or undoing actions.

  • Visitor: Applying different discount rules to product categories like electronics or clothing.

4. Financial Applications

  • Decorator: Adding additional functionality to transaction systems, such as logging, encryption, or fraud detection.

  • Proxy: Implementing access control or lazy loading for sensitive financial data.

  • Chain of Responsibility: Processing user requests for loan approvals, where each step involves credit checks, income verification, and risk assessment.

5. Artificial Intelligence

  • Interpreter: Parsing and evaluating mathematical or logical expressions in AI models.

  • Composite: Representing decision trees or hierarchical data structures in machine learning.

  • Builder: Constructing complex neural networks with configurable layers and hyperparameters.

6. Enterprise Software

  • Adapter: Integrating third-party services (e.g., payment gateways, APIs) with existing systems.

  • Facade: Simplifying access to complex subsystems like reporting engines or authentication services.

  • Bridge: Supporting multiple back-end technologies (e.g., SQL and NoSQL) with a consistent interface.

By tailoring patterns to specific challenges, developers can significantly improve the robustness and scalability of their applications.


7. Summary and Best Practices

Key Takeaways from Design Patterns

  1. Decouple Dependencies: Many patterns promote loose coupling, making systems more modular and easier to maintain.

  2. Promote Reusability: Patterns like Singleton and Factory Method encourage code reuse and avoid duplication.

  3. Enhance Scalability: Structural patterns such as Flyweight and Composite help scale applications efficiently.

  4. Simplify Complexity: Patterns like Facade and Mediator abstract away intricate subsystems, providing a cleaner interface.

Best Practices for Using Design Patterns

  1. Understand the Problem First:

    • Identify the recurring challenge before deciding on a pattern.

    • Don’t use a pattern just because it exists—solve the right problem.

  2. Start Simple:

    • Focus on a working solution first, then refactor into patterns when the design evolves.
  3. Combine Patterns When Necessary:

    • Many complex systems benefit from combining patterns (e.g., Composite with Visitor for hierarchical processing).
  4. Stay Flexible:

    • Patterns are guidelines, not rigid rules. Adapt them to suit your project’s needs.
  5. Leverage Python Features:

    • Python’s dynamic typing, first-class functions, and decorators often simplify the implementation of patterns.

    • Use built-in tools like iterators, context managers, and meta-programming where applicable.


Design patterns are not just about memorizing templates or copying code snippets—they are about cultivating a mindset that makes your code more modular, flexible, and maintainable. With these 23 patterns, you now have a comprehensive toolkit for tackling recurring design challenges in Python development.

When to Use Design Patterns

While design patterns are powerful, they should be used judiciously:

  • Don't over-engineer: Avoid using patterns for simple problems.

  • Start with simplicity: Focus on solving the problem first; refactor to patterns when needed.

  • Understand the problem domain: Patterns are not a one-size-fits-all solution; they shine in specific contexts.

Key Lessons

  1. Understand the Context: Each pattern solves a specific type of problem. Identify the challenge before applying a pattern.

  2. Refactor When Needed: Introduce patterns during refactoring to simplify complex logic.

  3. Collaborate Effectively: Patterns are a shared language among developers, helping teams communicate more effectively.

Applying Patterns in Real Projects

  • Iterate Gradually: Don’t try to force patterns into your project from the beginning. Let the design evolve naturally, and use patterns to refactor and simplify.

  • Learn by Practice: Theoretical understanding is crucial, but the real power of patterns comes from implementing them in real-world scenarios.

  • Combine Patterns: In complex systems, multiple patterns often work together. For example, a Composite pattern for a hierarchy can use a Visitor for operations like traversal.

Final Thoughts

Design patterns are tools to empower developers. They aren't meant to constrain creativity but to provide proven strategies for solving common problems. As you grow in your software engineering journey:

  • Keep revisiting these patterns.

  • Explore how they integrate with modern programming paradigms like functional programming and asynchronous systems.

  • Experiment with variations of these patterns to address your unique challenges.


Further Reading and Exploration

  1. Books to Explore:

    • Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma et al. (the "GoF" book).

    • Head First Design Patterns by Eric Freeman and Elisabeth Robson.

    • Python Design Patterns and Best Practices by Arun Ravindran.

  2. Hands-On Practice:

    • Refactor old projects by introducing design patterns.

    • Solve coding challenges with patterns in mind (e.g., LeetCode, Codewars).

  3. Explore Advanced Topics:

    • Combining patterns in a single architecture.

    • Implementing patterns in modern Python paradigms like asyncio and type annotations.

    • Using patterns in frameworks like Django or Flask.


This should serve as a good base for you to start writing cleaner, more maintainable, and scalable Python code. Patterns aren’t just about solving problems—they’re about solving them beautifully. Happy coding! 😊

Feel free to reach out to me at AhmadWKhan.com

Did you find this article valuable?

Support Ahmad W Khan by becoming a sponsor. Any amount is appreciated!