Python Design Patterns

Choose the simplest solution that works. Complexity must be justified by concrete requirements

What Is This

Python Design Patterns refers to a set of foundational principles and patterns that guide the structure and organization of your Python code. This skill emphasizes practical guidelines such as KISS (Keep It Simple), Separation of Concerns, the Single Responsibility Principle (SRP), and the preference for composition over inheritance. By applying these patterns, developers can create systems that are maintainable, easy to understand, and adaptable to change.

This skill is especially relevant when architecting new services or components, refactoring monolithic code, deciding whether to introduce new abstractions, or evaluating the structural quality of code in code reviews. The central goal is to minimize unnecessary complexity, ensure clear separation of responsibilities, and foster codebases that are easy to test.

Why Use It

Modern Python projects, especially as they grow, tend to accumulate complexity that can make them difficult to maintain and evolve. Common issues such as large "God classes," tangled dependencies, or mixing business logic with I/O can all contribute to fragile codebases. By leveraging design patterns and principles outlined in this skill, you can:

  • Reduce code duplication and improve reusability
  • Isolate changes so that bug fixes and updates do not introduce regressions
  • Improve testability by decoupling logic from external systems
  • Enable multiple developers to work on the same codebase without stepping on each other's toes
  • Simplify onboarding for new team members

Ultimately, this skill helps you justify every piece of complexity in your codebase, ensuring that your architecture supports your requirements without unnecessary overhead.

How to Use It

KISS (Keep It Simple)

The KISS principle advises developers to always choose the simplest implementation that fulfills the requirements. Avoid speculative abstractions or premature optimizations.

Example:

## Simple and clear function
def calculate_discount(price, percentage):
    return price * (1 - percentage / 100)

Avoid over-engineering:

## Unnecessarily complex for the same outcome
class DiscountCalculator:
    def __init__(self, strategy):
        self.strategy = strategy

    def apply(self, price, percentage):
        return self.strategy(price, percentage)

Unless you have multiple discount strategies, the first approach is preferable.

Single Responsibility Principle (SRP)

Each module, class, or function should have one reason to change. This means separating unrelated concerns.

Example:

class UserRepository:
    def save(self, user):
        # Save user to database
        pass

class UserEmailService:
    def send_welcome(self, user):
        # Send welcome email
        pass

This separation makes it easier to change the email logic without risking data layer regressions.

Separation of Concerns

Divide your code into distinct sections, each addressing a separate concern. This often pairs with SRP.

Example:

  • Data access logic in one module
  • Business logic in another
  • Presentation logic in yet another
## data_access.py
def fetch_user(user_id):
    # Fetch user from database
    pass

## business_logic.py
def register_user(user_data):
    # Register user using data_access
    pass

## presentation.py
def render_user_profile(user):
    # Render HTML or JSON
    pass

Composition Over Inheritance

Favor building functionality by combining simple, reusable components instead of relying on deep inheritance hierarchies. This avoids tight coupling and makes your code more flexible.

Example:

class Logger:
    def log(self, message):
        print(message)

class Service:
    def __init__(self, logger):
        self.logger = logger

    def process(self):
        self.logger.log("Processing...")

You can swap out Logger for a mock or a different implementation without modifying Service.

Evaluating Coupling and Abstractions

When reviewing code or considering changes, ask:

  • Are modules/classes tightly coupled?
  • Do small changes ripple through unrelated parts of the code?
  • Is there unnecessary leakage of internal types or responsibilities?

Modularize and decouple where it adds clarity and testability, but avoid introducing abstractions before they are justified by concrete requirements.

When to Use It

Apply the Python Design Patterns skill in the following scenarios:

  • Designing new components or services: Structure your code from the outset to favor simplicity, separation, and clear responsibilities.
  • Refactoring monolithic or tangled code: Break down large functions or classes to improve clarity and maintainability.
  • Deciding whether to introduce a new abstraction: Only create new layers or interfaces if you have a concrete use case.
  • Choosing between inheritance and composition: Use composition for flexibility unless inheritance is clearly justified.
  • Evaluating code during reviews: Identify issues such as tight coupling, large classes, or improper separation of concerns.
  • Improving testability: Separate I/O from business logic to facilitate unit testing.

Important Notes

  • Justify complexity: Only introduce new abstractions or design patterns when there is a clear and present need.
  • Avoid premature optimization: Focus on clarity and correctness first, then refactor as requirements become concrete.
  • Balance design rigor with pragmatism: Overuse of patterns can lead to unnecessarily complex code.
  • Favor readability and maintainability: Write code that is easy for others (and your future self) to understand and modify.
  • Iterate and adapt: Design patterns are guidelines, not strict rules. Adapt them to fit the context of your project.

By applying the principles in this skill, you can create Python codebases that are robust, maintainable, and scalable, while avoiding unnecessary architectural overhead.