Introduction to SOLID principles in Python

Excerpt: In this article, we explore the SOLID principlesβ€”five foundational software design principles that help Python developers write maintainable, scalable, and testable code. We’ll break down each principle with Python examples, discuss common pitfalls, and explain how major tech teams apply SOLID in real-world systems.

Understanding the SOLID Principles

The term SOLID was coined by Robert C. Martin (Uncle Bob) to represent five design principles that encourage good object-oriented programming practices. Even though Python is multi-paradigm, these principles apply well in large-scale Python systems where code readability and adaptability are crucial.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ S β†’ Single Responsibility Principle β”‚
β”‚ O β†’ Open/Closed Principle β”‚
β”‚ L β†’ Liskov Substitution Principle β”‚
β”‚ I β†’ Interface Segregation Principle β”‚
β”‚ D β†’ Dependency Inversion Principle β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

By mastering these concepts, you can avoid spaghetti code, reduce coupling, and ensure that refactoring doesn’t break existing behaviorβ€”critical for long-lived Python applications.

1. Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change. In practice, this means each class should focus on a single job.

Violating SRP is one of the most common design mistakes in Python projects. When a class handles multiple concerns (e.g., data processing, logging, and database access), it becomes difficult to test and maintain.

Bad Example:

class ReportManager:
 def generate_report(self, data):
 # Generate report logic
 return f"Report for {data}"

 def save_to_file(self, content, filename):
 with open(filename, 'w') as f:
 f.write(content)

 def send_email(self, email, content):
 # Email logic
 print(f"Sending email to {email}")

This class does three things: generates reports, writes files, and sends emails. Each task could change independently, violating SRP.

Better Example:

class ReportGenerator:
 def generate(self, data):
 return f"Report for {data}"

class FileSaver:
 def save(self, content, filename):
 with open(filename, 'w') as f:
 f.write(content)

class EmailSender:
 def send(self, email, content):
 print(f"Sending email to {email}")

Now, each class has a distinct purpose. Tools like pylint and flake8 can detect overly complex classes and methods, helping enforce SRP automatically.

2. Open/Closed Principle (OCP)

Definition: Software entities should be open for extension, but closed for modification.

In other words, you should be able to add new features without altering existing code. Python’s dynamic nature and use of abstract base classes (ABCs) make it straightforward to apply OCP.

Bad Example:

def calculate_discount(price, customer_type):
 if customer_type == 'regular':
 return price * 0.95
 elif customer_type == 'vip':
 return price * 0.90
 elif customer_type == 'employee':
 return price * 0.80

Each new customer type requires modifying this functionβ€”a direct violation of OCP.

Better Example (Using Polymorphism):

from abc import ABC, abstractmethod

class DiscountStrategy(ABC):
 @abstractmethod
 def apply(self, price):
 pass

class RegularDiscount(DiscountStrategy):
 def apply(self, price):
 return price * 0.95

class VIPDiscount(DiscountStrategy):
 def apply(self, price):
 return price * 0.90

class EmployeeDiscount(DiscountStrategy):
 def apply(self, price):
 return price * 0.80

def calculate_price(price, discount: DiscountStrategy):
 return discount.apply(price)

Adding a new discount now only requires subclassing DiscountStrategy. No existing code needs modification, keeping your system stable.

3. Liskov Substitution Principle (LSP)

Definition: Objects of a superclass should be replaceable with objects of its subclasses without breaking the system.

This principle ensures that inheritance models behavior correctly. Violations typically occur when subclass methods break parent expectations.

Bad Example:

class Bird:
 def fly(self):
 print("Flying...")

class Penguin(Bird):
 def fly(self):
 raise Exception("Penguins can't fly!")

A Penguin is a Bird, but substituting it breaks the system. This violates LSP.

Better Example:

class Bird(ABC):
 @abstractmethod
 def move(self):
 pass

class Sparrow(Bird):
 def move(self):
 print("Flying...")

class Penguin(Bird):
 def move(self):
 print("Swimming...")

Now both subclasses respect the parent contract by implementing move() appropriately. The base class describes expected behavior without assumptions that break subclasses.

4. Interface Segregation Principle (ISP)

Definition: Clients should not be forced to depend on interfaces they do not use.

Python doesn’t have interfaces in the traditional Java sense, but this principle applies to abstract base classes and large mixins.

Bad Example:

class Machine(ABC):
 @abstractmethod
 def print_doc(self, doc): pass

 @abstractmethod
 def scan_doc(self, doc): pass

 @abstractmethod
 def fax_doc(self, doc): pass

class BasicPrinter(Machine):
 def print_doc(self, doc):
 print("Printing...")
 def scan_doc(self, doc):
 raise NotImplementedError
 def fax_doc(self, doc):
 raise NotImplementedError

BasicPrinter is forced to implement methods it doesn’t need. This violates ISP.

Better Example:

class Printer(ABC):
 @abstractmethod
 def print_doc(self, doc): pass

class Scanner(ABC):
 @abstractmethod
 def scan_doc(self, doc): pass

class Fax(ABC):
 @abstractmethod
 def fax_doc(self, doc): pass

class BasicPrinter(Printer):
 def print_doc(self, doc):
 print("Printing...")

By splitting large interfaces, we ensure classes depend only on what they need. This modularity pays off in microservice-based architectures and plugin systems.

5. Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules; both should depend on abstractions.

DIP decouples code layers and is critical for testing and flexibility. Instead of creating dependencies directly, inject them via interfaces or constructors.

Bad Example:

class MySQLDatabase:
 def connect(self):
 print("Connecting to MySQL...")

class ReportService:
 def __init__(self):
 self.db = MySQLDatabase()

 def generate(self):
 self.db.connect()
 print("Generating report...")

Here, ReportService depends on a specific implementation (MySQLDatabase), making it hard to switch databases or mock connections in tests.

Better Example:

class Database(ABC):
 @abstractmethod
 def connect(self): pass

class MySQLDatabase(Database):
 def connect(self):
 print("Connecting to MySQL...")

class PostgresDatabase(Database):
 def connect(self):
 print("Connecting to Postgres...")

class ReportService:
 def __init__(self, db: Database):
 self.db = db

 def generate(self):
 self.db.connect()
 print("Generating report...")

# Dependency injected externally
db = PostgresDatabase()
service = ReportService(db)
service.generate()

Now, the high-level logic depends on the abstract Database, not a specific implementation. Frameworks like FastAPI and Flask adopt this pattern with dependency injection containers (e.g., punq, injector).

Industry Adoption

Modern Python systems use SOLID-inspired architectures extensively:

  • Netflix applies DIP and OCP for plugin-based data processing.
  • Spotify uses SRP and ISP in their internal ML services to separate data ingestion from model serving.
  • Instagram emphasizes LSP and OCP for backend modularization in their Django-based infrastructure.

Common Tools and Libraries Supporting SOLID Design

  • mypy and pyright: Type checking enforces interface-like contracts (ISP, LSP).
  • pydantic: Encourages SRP by isolating validation logic.
  • FastAPI: Built around DIP with dependency injection as a core feature.
  • pytest: Supports mocking for dependency inversion testing.

Summary Table

Principle Key Idea Applied Benefit
SRP Each class handles one concern Improved maintainability
OCP Extend without modifying existing code Safer updates and plugin flexibility
LSP Subtypes can replace base types safely Reliable polymorphism
ISP Split large interfaces into focused ones Reduced unnecessary dependencies
DIP Depend on abstractions, not concretions Testability and flexibility

Final Thoughts

The SOLID principles aren’t strict rulesβ€”they’re guiding philosophies that help shape good software design. In Python, where dynamic typing can blur boundaries, applying these ideas brings consistency and long-term maintainability. Whether you’re building a Flask API, a FastAPI microservice, or a machine learning pipeline, adopting SOLID practices from the start ensures your codebase remains robust, testable, and ready for growth.

Recommended Reading: