The SOLID principles are among the most important pillars of modern object-oriented programming. Coined by Robert C. Martin (Uncle Bob) in the early 2000s, these five principles have become the foundation for writing clean, flexible, and maintainable code in any object-oriented language — and Python is no exception.

If you have already studied Object-Oriented Programming in Python and want to take your code to the next level, this complete SOLID in Python guide is exactly what you need. We will explore each principle with practical examples, showing the code before and after applying each concept.

What Are the SOLID Principles?

SOLID is an acronym representing five object-oriented design principles:

  • SSingle Responsibility Principle (SRP)
  • OOpen/Closed Principle (OCP)
  • LLiskov Substitution Principle (LSP)
  • IInterface Segregation Principle (ISP)
  • DDependency Inversion Principle (DIP)

Together, these principles help developers build systems that are easier to understand, test, extend, and maintain. According to the freeCodeCamp guide on SOLID with Python, consistently applying these principles dramatically reduces coupling between modules and increases internal class cohesion.

The Wikipedia definition of SOLID describes it as a set of best practices that, when applied together, make developers more productive and code more resilient to change. Let us dive into each principle with real Python examples.

1. Single Responsibility Principle (SRP)

The Single Responsibility Principle states that a class should have only one reason to change. In other words, each class should be responsible for a single part of the system's functionality, fully encapsulating that responsibility.

This is the most fundamental of the SOLID principles. Robert C. Martin defines responsibility as "a reason to change." If a class has multiple responsibilities, it will have multiple reasons to change, making the system fragile and hard to maintain.

Bad Example — Violating SRP

class FinancialReport:
    def __init__(self, data):
        self.data = data
def calculate_totals(self):
    return sum(item['amount'] for item in self.data)

def generate_html(self):
    totals = self.calculate_totals()
    return f'<html><body><h1>Total: {totals}</h1></body></html>'

def save_to_file(self, path):
    with open(path, 'w') as f:
        f.write(self.generate_html())

This example violates SRP because the FinancialReport class has three distinct responsibilities: calculating financial data, generating HTML, and managing files. Each of these is a potential reason for future changes.

Correct Example — Applying SRP

class FinancialCalculator:
    def calculate_totals(self, data):
        return sum(item['amount'] for item in data)

class HTMLGenerator: def generate_report(self, totals): return f'<html><body><h1>Total: {totals}</h1></body></html>'

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

Now each class has a single, well-defined responsibility. FinancialCalculator handles only calculations, HTMLGenerator only formatting, and FileManager only persistence. Changes in one aspect do not affect the others. The Real Python article on SOLID principles emphasizes that this separation is essential for creating testable and maintainable systems.

2. Open/Closed Principle (OCP)

The Open/Closed Principle states that software entities (classes, modules, functions) should be open for extension but closed for modification. This means you should be able to add new behavior to a system without altering existing code.

The core idea behind OCP is to protect existing code from regressions while allowing the system to evolve. In Python, we can achieve this through inheritance, composition, and most elegantly through strategies leveraging first-class functions.

Bad Example — Violating OCP

class PaymentProcessor:
    def process(self, payment_type, amount):
        if payment_type == 'credit_card':
            print(f'Processing credit card payment: {amount}')
        elif payment_type == 'paypal':
            print(f'Processing PayPal payment: {amount}')
        elif payment_type == 'crypto':
            print(f'Processing cryptocurrency: {amount}')

Every time a new payment method is added, we must modify the PaymentProcessor class by adding another elif. This violates OCP and makes the class error-prone.

Correct Example — Applying OCP

from abc import ABC, abstractmethod

class PaymentMethod(ABC): @abstractmethod def process(self, amount): pass

class CreditCardPayment(PaymentMethod): def process(self, amount): print(f'Processing credit card payment: {amount}')

class PayPalPayment(PaymentMethod): def process(self, amount): print(f'Processing PayPal payment: {amount}')

class CryptoPayment(PaymentMethod): def process(self, amount): print(f'Processing cryptocurrency: {amount}')

class PaymentProcessor: def init(self, method: PaymentMethod): self.method = method

def process(self, amount):
    self.method.process(amount)

Now, to add a new payment method, just create a new class that inherits from PaymentMethod. The existing PaymentProcessor code remains unchanged. This pattern is known as the Strategy Pattern, one of the topics covered in our Design Patterns in Python guide.

3. Liskov Substitution Principle (LSP)

The Liskov Substitution Principle, introduced by Barbara Liskov in 1987, states that objects of a superclass should be replaceable with objects of its subclasses without affecting program correctness. In practical terms: if a function expects an object of the base class, it should work correctly with any subclass object.

This principle is often violated in Python when subclasses override methods in ways that change the expected behavior of the base class. The official Python classes documentation provides a solid foundation for understanding inheritance and polymorphism — essential concepts for applying LSP correctly.

Bad Example — Violating LSP

class Bird:
    def fly(self):
        return 'I am flying!'

class Penguin(Bird): def fly(self): raise NotImplementedError('Penguins cannot fly!')

def make_bird_fly(bird: Bird): return bird.fly()

penguin = Penguin() make_bird_fly(penguin) # Raises error!

The problem is that Penguin is a subclass of Bird but cannot properly substitute its parent. If the client code expects every bird to fly, the penguin breaks that expectation.

Correct Example — Applying LSP

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

class FlyingBird(Bird): def move(self): return 'Soaring through the skies!'

class SwimmingBird(Bird): def move(self): return 'Swimming through the waters!'

class Penguin(SwimmingBird): pass

class Eagle(FlyingBird): pass

def move_bird(bird: Bird): return bird.move()

Now the hierarchy correctly reflects the actual capabilities of each bird. LSP forces us to think in terms of behavior, not just taxonomy. Barbara Liskov received the Turing Award in 2008 for her foundational contributions to computer science, and this principle remains one of the most important in object-oriented design.

4. Interface Segregation Principle (ISP)

The Interface Segregation Principle states that a class should not be forced to implement interfaces it does not use. Instead of one large, generic interface, it is better to have several smaller, more specific interfaces.

In Python, we do not have interfaces in the traditional sense (as in Java or C#). However, we can use ABCs (Abstract Base Classes) and protocols to achieve the same goal. The standard library's abc module is the ideal tool for this. The PEP 8 — Python Style Guide recommends practices that naturally align with ISP.

Bad Example — Violating ISP

from abc import ABC, abstractmethod

class Worker(ABC): @abstractmethod def work(self): pass

@abstractmethod
def eat(self):
    pass

@abstractmethod
def sleep(self):
    pass

class Robot(Worker): def work(self): print('Working...')

def eat(self):
    raise NotImplementedError('Robots do not eat!')

def sleep(self):
    raise NotImplementedError('Robots do not sleep!')

Robot is forced to implement methods that make no sense for its domain. This violates ISP and creates fragile code.

Correct Example — Applying ISP

class Workable(ABC):
    @abstractmethod
    def work(self):
        pass

class Eatable(ABC): @abstractmethod def eat(self): pass

class Sleepable(ABC): @abstractmethod def sleep(self): pass

class Human(Workable, Eatable, Sleepable): def work(self): print('Human working...')

def eat(self):
    print('Human eating...')

def sleep(self):
    print('Human sleeping...')

class Robot(Workable): def work(self): print('Robot working...')

Now each class implements only the interfaces relevant to its context. ISP promotes cohesion and prevents changes in one interface from propagating to unrelated classes. The Refactoring Guru guide on SOLID offers an excellent visual reference on how to apply this and the other principles.

5. Dependency Inversion Principle (DIP)

The Dependency Inversion Principle establishes two fundamental concepts:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

In other words, your code should depend on interfaces or abstract classes, not on concrete implementations. This allows you to swap implementations without modifying client code.

Bad Example — Violating DIP

class MySQLDatabase:
    def connect(self):
        print('Connecting to MySQL...')
def save(self, data):
    print(f'Saving to MySQL: {data}')

class UserService: def init(self): self.db = MySQLDatabase()

def register(self, name, email):
    self.db.connect()
    self.db.save({'name': name, 'email': email})

UserService directly depends on the concrete implementation MySQLDatabase. If we want to migrate to PostgreSQL, we would need to modify the class.

Correct Example — Applying DIP

from abc import ABC, abstractmethod

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

@abstractmethod
def save(self, data):
    pass

class MySQLRepository(Repository): def connect(self): print('Connecting to MySQL...')

def save(self, data):
    print(f'Saving to MySQL: {data}')

class PostgreSQLRepository(Repository): def connect(self): print('Connecting to PostgreSQL...')

def save(self, data):
    print(f'Saving to PostgreSQL: {data}')

class UserService: def init(self, repository: Repository): self.repository = repository

def register(self, name, email):
    self.repository.connect()
    self.repository.save({'name': name, 'email': email})

Now UserService depends on the Repository abstraction, not on a specific implementation. We can swap the database without altering the service. This is the essence of dependency injection, a fundamental pattern in professional development. The Real Python article on inheritance vs composition explores how DIP relates to these fundamental concepts.

Best Practices When Applying SOLID in Python

Mastering the SOLID principles does not happen overnight. Here are some practical recommendations to get started:

  • Start with SRP: It is the easiest to understand and delivers the greatest immediate impact. When creating a class, ask yourself: "What is the single responsibility of this class?"
  • Use type hints: Type hints in Python help document type expectations and make it easier to apply LSP and DIP. Our complete guide to Type Hints in Python covers everything you need to know.
  • Prefer composition over inheritance: Composition makes your code more flexible and facilitates the application of OCP and DIP.
  • Do not force SOLID on simple scripts: SOLID principles are most valuable in complex systems. In small scripts, simplicity should come first.
  • Use dependency injection: Frameworks like FastAPI and Django already encourage this pattern. Your code will be more testable and flexible.

SOLID and Testing

One of the greatest benefits of applying SOLID is code testability. Classes with single responsibility are easy to test in isolation. Dependency inversion allows you to inject mocks and stubs with ease. The GeeksforGeeks article on SOLID with real-life examples demonstrates how each principle contributes to more testable code.

If you are getting started with automated testing, our pytest guide for automated testing will help you write efficient tests for your SOLID classes.

Conclusion

The SOLID principles are not absolute rules, but valuable guidelines that help developers navigate the complexities of object-oriented design. In Python, where flexibility is one of the language's greatest virtues, applying SOLID requires discipline and practice, but the benefits are immense:

  • Cleaner code — each class has a clear and well-defined purpose
  • Greater reusability — decoupled classes can be used in different contexts
  • Ease of maintenance — changes in one part of the system do not propagate to others
  • Superior testability — isolated classes are much easier to test
  • Team collaboration — SOLID-compliant code is more predictable and easier for other developers to understand

Remember: the goal is not to apply every principle all the time, but to understand when and where each one makes sense. As Uncle Bob said, "SOLID is not about being perfect, it is about being better than yesterday."

To continue your studies, we recommend exploring our complete guide to OOP in Python and the Design Patterns in Python tutorial, which perfectly complement what you have learned here.