Mastering Code Architecture: Refactoring Python with SOLID Principles

Overview

Writing software that lasts requires more than just making code run; it requires making code maintainable. The

principles, popularized by
Robert C. Martin
(Uncle Bob), provide a roadmap for creating robust, flexible, and decoupled
Object-Oriented Programming
systems. By applying these five design rules, developers transform brittle, "spaghetti" code into a modular architecture where changes in one area don't trigger a cascade of failures elsewhere. This tutorial refactors a standard
Python
sales system to demonstrate these principles in action.

Prerequisites

To get the most out of this guide, you should possess a firm grasp of Python basics, particularly

,
Inheritance
, and
Method
overriding. Familiarity with the concept of
Abstract Base Class
will help when we move toward interface design.

Mastering Code Architecture: Refactoring Python with SOLID Principles
Uncle Bob’s SOLID Principles Made Easy 🍀 - In Python!

Key Libraries & Tools

  • abc
    : Python’s built-in module for defining Abstract Base Classes, essential for enforcing interface contracts.
  • Python
    3.x
    : The primary runtime environment for executing our refactored logic.

Code Walkthrough

1. Single Responsibility (SRP)

We start with an Order class that handles items and payment logic. This is high coupling. We extract the payment logic into a separate PaymentProcessor class. Now, Order only tracks items, while PaymentProcessor handles the transaction.

class Order:
    def __init__(self):
        self.items = []
        self.status = "open"

class PaymentProcessor:
    def pay_debit(self, order, security_code):
        print(f"Paying debit: {security_code}")
        order.status = "paid"

2. Open/Closed (OCP)

Adding new payment methods shouldn't mean modifying existing classes. We use the

module to create an abstract interface. Now, adding Bitcoin or PayPal simply requires a new subclass.

from abc import ABC, abstractmethod

class PaymentProcessor(ABC):
    @abstractmethod
    def pay(self, order):
        pass

3. Liskov Substitution (LSP)

If a Paypal subclass requires an email but a Debit subclass requires a security code, changing the pay method's signature breaks the program. We solve this by moving specific credentials to the __init__ method, keeping the pay method signature identical across all subtypes.

4. Interface Segregation & Dependency Inversion (ISP/DIP)

Instead of forcing every processor to implement Two-Factor Authentication (2FA), we use Composition. We create an Authorizer class and inject it into the processors. This follows DIP: our high-level payment logic depends on an abstract Authorizer rather than a concrete SMS_Authorizer.

class DebitPaymentProcessor(PaymentProcessor):
    def __init__(self, security_code, authorizer: Authorizer):
        self.authorizer = authorizer
        self.security_code = security_code

    def pay(self, order):
        if not self.authorizer.is_authorized():
            raise Exception("Not authorized")
        order.status = "paid"

Syntax Notes

Python uses the @abstractmethod decorator to identify methods that subclasses MUST implement. Note the use of Type Hinting (e.g., authorizer: Authorizer) which, while not enforced at runtime, is vital for clarity and IDE support in complex architectures.

Practical Examples

These principles shine in e-commerce backends, plugin systems, and API integrations. If you need to support multiple shipping carriers (FedEx, UPS, DHL), SRP and OCP allow you to add new carriers without touching the core shipping logic.

Tips & Gotchas

Avoid over-engineering. While

is powerful, applying it to a three-line script adds unnecessary complexity. Prefer Composition over Inheritance to avoid deep, confusing class hierarchies. If a subclass method is just raising a NotImplementedError, you are likely violating the Liskov Substitution Principle.

3 min read