py4u guide

Implementing Design Patterns in Python: A Practical Approach

Design patterns are reusable solutions to common software design problems. They are not code snippets but **templates** for solving specific challenges in object-oriented programming (OOP), such as structuring classes, managing dependencies, or optimizing communication between objects. For Python developers, understanding design patterns is critical for writing clean, maintainable, and scalable code—especially in large projects where structure and readability matter most. Python’s flexibility (e.g., dynamic typing, first-class functions, and built-in abstractions like decorators) makes implementing design patterns both intuitive and sometimes different from statically typed languages like Java or C++. This blog takes a hands-on approach to demystify design patterns, focusing on **practical use cases** and **Python-specific implementations**. Whether you’re a beginner looking to level up your OOP skills or an experienced developer aiming to refactor messy code, this guide will help you apply patterns effectively.

Table of Contents

  1. What Are Design Patterns?
  2. Why Use Design Patterns in Python?
  3. Creational Design Patterns
  4. Structural Design Patterns
  5. Behavioral Design Patterns
  6. When Not to Use Design Patterns
  7. Conclusion
  8. References

What Are Design Patterns?

Design patterns were popularized by the “Gang of Four” (GoF) in their 1994 book Design Patterns: Elements of Reusable Object-Oriented Software. They are categorized into three main types:

  • Creational Patterns: Focus on object creation mechanisms, ensuring flexibility and reuse of existing code (e.g., Singleton, Factory Method).
  • Structural Patterns: Deal with object composition, defining how classes/objects interact to form larger structures (e.g., Adapter, Decorator).
  • Behavioral Patterns: Manage communication and responsibility between objects (e.g., Observer, Strategy).

Why Use Design Patterns in Python?

Python’s “batteries-included” philosophy and dynamic nature mean many problems can be solved with built-in tools (e.g., collections for data structures, itertools for iteration). However, design patterns add value by:

  • Standardizing Solutions: Using patterns makes your code more readable to other developers familiar with OOP principles.
  • Reducing Redundancy: Avoid reinventing the wheel for common problems like “how to ensure only one instance of a class exists” (Singleton).
  • Improving Scalability: Patterns like Factory Method or Strategy make it easier to extend functionality without rewriting core code.

Creational Design Patterns

Creational patterns abstract the object creation process, decoupling the act of creating objects from the code that uses them. This flexibility is especially useful in Python, where dynamic object creation is common.

1. Singleton Pattern

Intent: Ensure a class has only one instance and provide a global point of access to it.

Problem: You need a single shared resource (e.g., a database connection pool, configuration manager) to avoid redundant setup/teardown or conflicting state.

Python Implementation:

Python has multiple ways to implement Singletons. The most robust is using a metaclass (since metaclasses control class creation).

class SingletonMeta(type):
    """Metaclass to enforce Singleton behavior."""
    _instances = {}  # Track instances of classes using this metaclass

    def __call__(cls, *args, **kwargs):
        """Override the instance creation logic."""
        if cls not in cls._instances:
            # Create the instance only if it doesn't exist
            instance = super().__call__(*args, **kwargs)
            cls._instances[cls] = instance
        return cls._instances[cls]


class DatabaseConfig(metaclass=SingletonMeta):
    """A Singleton class to manage database configuration."""
    def __init__(self, host: str, port: int):
        self.host = host
        self.port = port
        print(f"DatabaseConfig initialized with host={host}, port={port}")

    def get_connection_string(self) -> str:
        return f"postgresql://{self.host}:{self.port}/mydb"


# Usage
config1 = DatabaseConfig("localhost", 5432)
config2 = DatabaseConfig("example.com", 5433)  # Ignored; uses existing instance

print(config1 is config2)  # Output: True (both are the same instance)
print(config1.get_connection_string())  # Output: postgresql://localhost:5432/mydb

Key Notes:

  • The SingletonMeta metaclass overrides __call__, ensuring only one instance is created.
  • A simpler alternative is using a module (since Python modules are singletons by default). For example, a config.py module with global variables acts as a Singleton.
  • Pitfall: Singletons can make testing harder (due to shared state) and introduce hidden dependencies. Use them sparingly!

2. Factory Method Pattern

Intent: Define an interface for creating an object but let subclasses decide which class to instantiate.

Problem: You need to delegate object creation to subclasses to avoid tight coupling between a creator class and its products.

Example: A payment processing system that supports multiple gateways (Stripe, PayPal). The core logic shouldn’t hardcode gateway-specific classes.

Python Implementation:

from abc import ABC, abstractmethod

# ----------------------
# Product Interface: All payment gateways must implement this
# ----------------------
class PaymentGateway(ABC):
    @abstractmethod
    def process_payment(self, amount: float) -> str:
        pass


# ----------------------
# Concrete Products: Stripe and PayPal implementations
# ----------------------
class StripeGateway(PaymentGateway):
    def process_payment(self, amount: float) -> str:
        return f"Stripe: Processing payment of ${amount:.2f}"


class PayPalGateway(PaymentGateway):
    def process_payment(self, amount: float) -> str:
        return f"PayPal: Processing payment of ${amount:.2f}"


# ----------------------
# Creator: Defines the Factory Method
# ----------------------
class PaymentProcessor(ABC):
    @abstractmethod
    def create_gateway(self) -> PaymentGateway:
        """Factory Method: Subclasses implement this to return a PaymentGateway."""
        pass

    def pay(self, amount: float) -> str:
        """Core logic that uses the Factory Method's product."""
        gateway = self.create_gateway()
        return gateway.process_payment(amount)


# ----------------------
# Concrete Creators: Stripe and PayPal processors
# ----------------------
class StripeProcessor(PaymentProcessor):
    def create_gateway(self) -> PaymentGateway:
        return StripeGateway()


class PayPalProcessor(PaymentProcessor):
    def create_gateway(self) -> PaymentGateway:
        return PayPalGateway()


# Usage
if __name__ == "__main__":
    stripe_processor = StripeProcessor()
    print(stripe_processor.pay(99.99))  # Output: Stripe: Processing payment of $99.99

    paypal_processor = PayPalProcessor()
    print(paypal_processor.pay(49.99))  # Output: PayPal: Processing payment of $49.99

Key Notes:

  • The PaymentProcessor (Creator) delegates gateway creation to subclasses via create_gateway (the Factory Method).
  • Adding a new gateway (e.g., BitcoinGateway) only requires:
    1. A new BitcoinGateway class implementing PaymentGateway.
    2. A new BitcoinProcessor subclass of PaymentProcessor overriding create_gateway.
  • Python’s dynamic typing simplifies this pattern—no need for strict interfaces (though abc.ABC adds clarity).

3. Builder Pattern

Intent: Separate the construction of a complex object from its representation, allowing the same construction process to create different representations.

Problem: You need to build an object with many optional components (e.g., a Computer with CPU, RAM, storage, and optional GPU/keyboard). Constructing it via a constructor with 10+ parameters is messy.

Python Implementation:

class Computer:
    """Product: The complex object being built."""
    def __init__(self):
        self.cpu = None
        self.ram = None
        self.storage = None
        self.gpu = None  # Optional
        self.keyboard = None  # Optional

    def __str__(self):
        return (f"Computer Specs:\n"
                f"CPU: {self.cpu}\n"
                f"RAM: {self.ram}\n"
                f"Storage: {self.storage}\n"
                f"GPU: {self.gpu if self.gpu else 'None'}\n"
                f"Keyboard: {self.keyboard if self.keyboard else 'None'}")


class ComputerBuilder:
    """Builder: Defines steps to build the Computer."""
    def __init__(self):
        self.computer = Computer()  # Start with an empty product

    def set_cpu(self, cpu: str) -> "ComputerBuilder":
        self.computer.cpu = cpu
        return self  # Enable method chaining

    def set_ram(self, ram: str) -> "ComputerBuilder":
        self.computer.ram = ram
        return self

    def set_storage(self, storage: str) -> "ComputerBuilder":
        self.computer.storage = storage
        return self

    def add_gpu(self, gpu: str) -> "ComputerBuilder":
        self.computer.gpu = gpu
        return self

    def add_keyboard(self, keyboard: str) -> "ComputerBuilder":
        self.computer.keyboard = keyboard
        return self

    def build(self) -> Computer:
        """Return the final product."""
        return self.computer


# Usage
if __name__ == "__main__":
    # Build a gaming PC with all options
    gaming_pc = (ComputerBuilder()
                 .set_cpu("Intel i9")
                 .set_ram("32GB DDR5")
                 .set_storage("2TB NVMe")
                 .add_gpu("NVIDIA RTX 4090")
                 .add_keyboard("Mechanical RGB")
                 .build())

    print(gaming_pc)
    # Output:
    # Computer Specs:
    # CPU: Intel i9
    # RAM: 32GB DDR5
    # Storage: 2TB NVMe
    # GPU: NVIDIA RTX 4090
    # Keyboard: Mechanical RGB

Key Notes:

  • The ComputerBuilder provides fluent interfaces (method chaining via return self), making construction readable.
  • A Director class can be added to encapsulate common build sequences (e.g., GamingPCDirector or OfficePCDirector).
  • Python’s keyword arguments (**kwargs) are often used as a lightweight alternative, but the Builder pattern is better for objects with many optional steps.

Structural Design Patterns

Structural patterns focus on composing classes or objects to form larger structures, such as adapters for incompatible interfaces or decorators for dynamic functionality.

1. Adapter Pattern

Intent: Convert the interface of a class into another interface clients expect. Lets classes work together that couldn’t otherwise because of incompatible interfaces.

Problem: You need to integrate a legacy system (e.g., a LegacyPaymentProcessor with a charge_customer method) into a new system that expects a PaymentGateway interface with a process_payment method.

Python Implementation:

# ----------------------
# Legacy Component (Incompatible Interface)
# ----------------------
class LegacyPaymentProcessor:
    """Legacy system with a non-standard method name."""
    def charge_customer(self, customer_id: int, amount: float) -> str:
        return f"Legacy: Charged customer {customer_id} ${amount:.2f}"


# ----------------------
# Target Interface (What the new system expects)
# ----------------------
class PaymentGateway(ABC):
    @abstractmethod
    def process_payment(self, amount: float) -> str:
        pass


# ----------------------
# Adapter: Wraps LegacyPaymentProcessor to match PaymentGateway
# ----------------------
class LegacyAdapter(PaymentGateway):
    def __init__(self, legacy_processor: LegacyPaymentProcessor, customer_id: int):
        self.legacy_processor = legacy_processor
        self.customer_id = customer_id  # Adapter-specific state

    def process_payment(self, amount: float) -> str:
        # Adapt the new interface to the legacy method
        return self.legacy_processor.charge_customer(self.customer_id, amount)


# Usage
if __name__ == "__main__":
    legacy_processor = LegacyPaymentProcessor()
    adapter = LegacyAdapter(legacy_processor, customer_id=123)  # Wrap legacy system

    # New system calls process_payment (target interface)
    result = adapter.process_payment(49.99)
    print(result)  # Output: Legacy: Charged customer 123 $49.99

Key Notes:

  • The adapter acts as a middleman, translating calls between the target interface and the legacy component.
  • Python’s dynamic typing makes adapters lightweight—no need for explicit interface inheritance (though using ABC improves clarity).

2. Decorator Pattern

Intent: Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

Problem: You need to add features to an object (e.g., logging, caching, or validation) without modifying its code or creating a bloated subclass hierarchy.

Python Implementation:

Python has built-in support for decorators via the @decorator syntax, but here we’ll implement the OOP version for clarity.

from abc import ABC, abstractmethod

# ----------------------
# Component Interface: Defines the core functionality
# ----------------------
class Coffee(ABC):
    @abstractmethod
    def cost(self) -> float:
        pass

    @abstractmethod
    def description(self) -> str:
        pass


# ----------------------
# Concrete Component: Base implementation
# ----------------------
class Espresso(Coffee):
    def cost(self) -> float:
        return 2.50

    def description(self) -> str:
        return "Espresso"


# ----------------------
# Decorator: Wraps a Coffee and adds functionality
# ----------------------
class CoffeeDecorator(Coffee):
    def __init__(self, coffee: Coffee):
        self._coffee = coffee  # Wrap the coffee object

    @abstractmethod
    def cost(self) -> float:
        pass

    @abstractmethod
    def description(self) -> str:
        pass


# ----------------------
# Concrete Decorators: Add milk, sugar, etc.
# ----------------------
class MilkDecorator(CoffeeDecorator):
    def cost(self) -> float:
        return self._coffee.cost() + 0.50  # Add milk cost

    def description(self) -> str:
        return f"{self._coffee.description()}, Milk"


class SugarDecorator(CoffeeDecorator):
    def cost(self) -> float:
        return self._coffee.cost() + 0.25  # Add sugar cost

    def description(self) -> str:
        return f"{self._coffee.description()}, Sugar"


# Usage
if __name__ == "__main__":
    # Start with a base espresso
    coffee = Espresso()
    print(f"Base: {coffee.description()} - ${coffee.cost():.2f}")  # Output: Base: Espresso - $2.50

    # Add milk
    coffee_with_milk = MilkDecorator(coffee)
    print(f"With Milk: {coffee_with_milk.description()} - ${coffee_with_milk.cost():.2f}")  # Output: With Milk: Espresso, Milk - $3.00

    # Add sugar to the milk coffee
    coffee_with_milk_sugar = SugarDecorator(coffee_with_milk)
    print(f"With Milk & Sugar: {coffee_with_milk_sugar.description()} - ${coffee_with_milk_sugar.cost():.2f}")  # Output: With Milk & Sugar: Espresso, Milk, Sugar - $3.25

Key Notes:

  • Python’s function decorators (e.g., @log, @cache) are a language-specific implementation of this pattern for functions/methods.
  • Decorators can be stacked (e.g., @log + @cache), making them ideal for adding layers of functionality dynamically.

3. Composite Pattern

Intent: Compose objects into tree structures to represent part-whole hierarchies. Clients treat individual objects and compositions uniformly.

Problem: You need to work with hierarchical data (e.g., file systems: files and directories) where both leaves (files) and composites (directories) should be treated the same.

Python Implementation:

from abc import ABC, abstractmethod
from typing import List

# ----------------------
# Component Interface: Files and directories share this
# ----------------------
class FileSystemComponent(ABC):
    @abstractmethod
    def get_size(self) -> int:
        """Return the size (in bytes) of the component."""
        pass

    @abstractmethod
    def get_name(self) -> str:
        """Return the name of the component."""
        pass


# ----------------------
# Leaf: Represents a file (no children)
# ----------------------
class File(FileSystemComponent):
    def __init__(self, name: str, size: int):
        self._name = name
        self._size = size

    def get_size(self) -> int:
        return self._size

    def get_name(self) -> str:
        return self._name


# ----------------------
# Composite: Represents a directory (has children)
# ----------------------
class Directory(FileSystemComponent):
    def __init__(self, name: str):
        self._name = name
        self._children: List[FileSystemComponent] = []  # Contains files/directories

    def add_child(self, child: FileSystemComponent) -> None:
        self._children.append(child)

    def remove_child(self, child: FileSystemComponent) -> None:
        self._children.remove(child)

    def get_size(self) -> int:
        """Sum the sizes of all children (recursive)."""
        return sum(child.get_size() for child in self._children)

    def get_name(self) -> str:
        return self._name


# Usage
if __name__ == "__main__":
    # Create files
    file1 = File("document.txt", 1024)  # 1KB
    file2 = File("image.jpg", 20480)    # 20KB

    # Create a subdirectory with a file
    subdir = Directory("photos")
    subdir.add_child(File("vacation.jpg", 51200))  # 50KB

    # Create root directory and add components
    root = Directory("root")
    root.add_child(file1)
    root.add_child(file2)
    root.add_child(subdir)

    # Calculate total size (uniform treatment of files/directories)
    print(f"Root directory size: {root.get_size()} bytes")  # Output: Root directory size: 72704 bytes

Key Notes:

  • The FileSystemComponent interface ensures both File (leaf) and Directory (composite) implement get_size and get_name.
  • Recursion is critical here: composites like Directory delegate work to their children.

Behavioral Design Patterns

Behavioral patterns manage communication and collaboration between objects, ensuring flexible and efficient interaction.

1. Observer Pattern

Intent: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

Problem: You need to notify multiple objects (e.g., UI widgets, logs, analytics) when a central state changes (e.g., a sensor reading, stock price update).

Python Implementation:

from abc import ABC, abstractmethod
from typing import List

# ----------------------
# Subject Interface: Notifies observers of state changes
# ----------------------
class Subject(ABC):
    @abstractmethod
    def attach(self, observer: "Observer") -> None:
        """Add an observer to the list."""
        pass

    @abstractmethod
    def detach(self, observer: "Observer") -> None:
        """Remove an observer from the list."""
        pass

    @abstractmethod
    def notify(self) -> None:
        """Notify all observers of a state change."""
        pass


# ----------------------
# Observer Interface: Defines a reaction to state changes
# ----------------------
class Observer(ABC):
    @abstractmethod
    def update(self, subject: Subject) -> None:
        """Update observer state based on the subject's new state."""
        pass


# ----------------------
# Concrete Subject: Weather station with temperature data
# ----------------------
class WeatherStation(Subject):
    def __init__(self):
        self._temperature = 0.0
        self._observers: List[Observer] = []  # Track attached observers

    @property
    def temperature(self) -> float:
        return self._temperature

    @temperature.setter
    def temperature(self, value: float) -> None:
        self._temperature = value
        self.notify()  # Notify observers when temperature changes

    def attach(self, observer: Observer) -> None:
        if observer not in self._observers:
            self._observers.append(observer)

    def detach(self, observer: Observer) -> None:
        self._observers.remove(observer)

    def notify(self) -> None:
        """Trigger update on all observers."""
        for observer in self._observers:
            observer.update(self)


# ----------------------
# Concrete Observers: Display devices
# ----------------------
class PhoneDisplay(Observer):
    def update(self, subject: WeatherStation) -> None:
        print(f"Phone Display: Current temp is {subject.temperature}°C")


class LaptopDisplay(Observer):
    def update(self, subject: WeatherStation) -> None:
        print(f"Laptop Display: Temperature updated to {subject.temperature}°C")


# Usage
if __name__ == "__main__":
    station = WeatherStation()
    phone = PhoneDisplay()
    laptop = LaptopDisplay()

    # Attach observers
    station.attach(phone)
    station.attach(laptop)

    # Change temperature (triggers notifications)
    station.temperature = 22.5
    # Output:
    # Phone Display: Current temp is 22.5°C
    # Laptop Display: Temperature updated to 22.5°C

    # Detach phone and update again
    station.detach(phone)
    station.temperature = 25.0
    # Output:
    # Laptop Display: Temperature updated to 25.0°C

Key Notes:

  • The WeatherStation (subject) notifies all attached observers via notify() when its temperature changes.
  • Python’s typing module and ABC ensure type safety, but you could also use duck typing (e.g., checking if observer has an update method).

2. Strategy Pattern

Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

Problem: You need to support multiple variants of an algorithm (e.g., sorting, payment processing) and switch between them dynamically.

Python Implementation:

from abc import ABC, abstractmethod

# ----------------------
# Strategy Interface: Defines the algorithm contract
# ----------------------
class SortStrategy(ABC):
    @abstractmethod
    def sort(self, data: list) -> list:
        pass


# ----------------------
# Concrete Strategies: Different sorting algorithms
# ----------------------
class BubbleSort(SortStrategy):
    def sort(self, data: list) -> list:
        data_copy = data.copy()
        n = len(data_copy)
        for i in range(n):
            for j in range(0, n-i-1):
                if data_copy[j] > data_copy[j+1]:
                    data_copy[j], data_copy[j+1] = data_copy[j+1], data_copy[j]
        return data_copy


class QuickSort(SortStrategy):
    def sort(self, data: list) -> list:
        data_copy = data.copy()
        if len(data_copy) <= 1:
            return data_copy
        pivot = data_copy[len(data_copy) // 2]
        left = [x for x in data_copy if x < pivot]
        middle = [x for x in data_copy if x == pivot]
        right = [x for x in data_copy if x > pivot]
        return self.sort(left) + middle + self.sort(right)


# ----------------------
# Context: Uses a strategy to perform work
# ----------------------
class Sorter:
    def __init__(self, strategy: SortStrategy):
        self._strategy = strategy  # Inject the strategy

    def set_strategy(self, strategy: SortStrategy) -> None:
        """Dynamically change the strategy."""
        self._strategy = strategy

    def sort_data(self, data: list) -> list:
        """Delegate sorting to the strategy."""
        return self._strategy.sort(data)


# Usage
if __name__ == "__main__":
    data = [3, 1, 4, 1, 5, 9, 2, 6]

    # Use Bubble Sort
    bubble_sorter = Sorter(BubbleSort())
    print("Bubble Sort:", bubble_sorter.sort_data(data))  # Output: Bubble Sort: [1, 1, 2, 3, 4, 5, 6, 9]

    # Switch to QuickSort dynamically
    bubble_sorter.set_strategy(QuickSort())
    print("Quick Sort:", bubble_sorter.sort_data(data))   # Output: Quick Sort: [1, a1, 2, 3, 4, 5, 6, 9]

Key Notes:

  • The Sorter (context) is decoupled from the sorting logic, making it easy to add new strategies (e.g., MergeSort).
  • Python’s first-class functions simplify this pattern: you can pass functions directly as strategies (no need for classes). For example:
    def bubble_sort(data): ... 
    def quick_sort(data): ... 
    sorter = Sorter(strategy=quick_sort)  # Use a function as the strategy

3. Command Pattern

Intent: Encapsulate a request as an object, thereby allowing for parameterization of clients with queues, requests, and operations.

Problem: You need to decouple the sender of a request (e.g., a button) from the receiver (e.g., a light, TV) and support features like undo/redo or logging requests.

Python Implementation:

from abc import ABC, abstractmethod

# ----------------------
# Command Interface: Defines the execute/undo contract
# ----------------------
class Command(ABC):
    @abstractmethod
    def execute(self) -> None:
        pass

    @abstractmethod
    def undo(self) -> None:
        pass


# ----------------------
# Receiver: The object that performs the actual work
# ----------------------
class Light:
    def turn_on(self) -> None:
        print("Light is ON")

    def turn_off(self) -> None:
        print("Light is OFF")


# ----------------------
# Concrete Commands: Encapsulate requests for the Light
# ----------------------
class LightOnCommand(Command):
    def __init__(self, light: Light):
        self._light = light  # Reference to the receiver

    def execute(self) -> None:
        self._light.turn_on()

    def undo(self) -> None:
        self._light.turn_off()  # Undo: reverse the execute action


class LightOffCommand(Command):
    def __init__(self, light: Light):
        self._light = light

    def execute(self) -> None:
        self._light.turn_off()

    def undo(self) -> None:
        self._light.turn_on()


# ----------------------
# Invoker: Sends commands to receivers (e.g., a remote control)
# ----------------------
class RemoteControl:
    def __init__(self):
        self._command = None  # Currently selected command

    def set_command(self, command: Command) -> None:
        self._command = command

    def press_button(self) -> None:
        """Execute the current command."""
        if self._command:
            self._command.execute()

    def press_undo_button(self) -> None:
        """Undo the last command."""
        if self._command:
            self._command.undo()


# Usage
if __name__ == "__main__":
    # Setup: Receiver (light) and commands
    living_room_light = Light()
    light_on = LightOnCommand(living_room_light)
    light_off = LightOffCommand(living_room_light)

    # Invoker: Remote control
    remote = RemoteControl()

    # Press "ON" button
    remote.set_command(light_on)
    remote.press_button()  # Output: Light is ON

    # Press "OFF" button
    remote.set_command(light_off)
    remote.press_button()  # Output: Light is OFF

    # Undo last action (turn off → turn on)
    remote.press_undo_button()  # Output: Light is ON

Key Notes:

  • Commands encapsulate what to do (e.g., turn_on) and who to do it to (the receiver, Light).
  • This pattern enables advanced features like command queues (e.g., batch processing) or macros (sequences of commands).

When Not to Use Design Patterns

Design patterns are powerful, but they can be overused. Avoid them when:

  • The problem is trivial (e.g., a simple script with 100 lines of code).
  • They introduce unnecessary complexity (e.g., using Singleton for a stateless utility class).
  • Python’s built-in features solve the problem better (e.g., using collections.defaultdict instead of a custom Factory pattern).

Conclusion

Design patterns are not silver bullets, but they are invaluable tools for solving recurring design problems. In Python, their implementation is often simplified by the language’s flexibility—whether through metaclasses for Singletons, decorators for dynamic behavior, or first-class functions for Strategies.

The key to mastering design patterns is practice: identify problems in your code, recognize which pattern applies, and refactor incrementally. Over time, you’ll develop an intuition for when to use patterns to write cleaner, more maintainable Python code.

References