Python SOLID Principles

The SOLID principles are a set of five design principles that help developers create maintainable, scalable, and robust software. These principles are essential for writing clean and efficient object-oriented code in Python.

What are SOLID Principles?

The term SOLID is an acronym that represents the five fundamental principles of object-oriented programming:

PrincipleDescription
S – Single Responsibility Principle (SRP)A class should have only one reason to change, meaning it should have only one job or responsibility.
O – Open/Closed Principle (OCP)Software entities (classes, modules, functions) should be open for extension but closed for modification.
L – Liskov Substitution Principle (LSP)Derived classes should be substitutable for their base classes without altering the correctness of the program.
I – Interface Segregation Principle (ISP)Clients should not be forced to depend on interfaces they do not use.
D – Dependency Inversion Principle (DIP)High-level modules should not depend on low-level modules. Instead, both should depend on abstractions.

Examples

1. Single Responsibility Principle (SRP)

The Single Responsibility Principle states that a class should have only one reason to change. This means that a class should have a single responsibility or function.

In this example, we separate the responsibilities of handling user data and saving it to a file. This makes the code more modular and easier to maintain.

</>
Copy
class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

    def get_user_info(self):
        return f"User: {self.name}, Email: {self.email}"


class UserDataSaver:
    def save_to_file(self, user):
        with open("user_data.txt", "w") as file:
            file.write(user.get_user_info())

# Creating a user instance
user = User("Alice", "alice@example.com")

# Saving user data
saver = UserDataSaver()
saver.save_to_file(user)

Here, the User class is responsible only for storing user information, while UserDataSaver handles file operations. This separation of concerns makes the code more maintainable.

2. Open/Closed Principle (OCP)

The Open/Closed Principle states that a class should be open for extension but closed for modification. This means that we should be able to add new functionality without modifying existing code.

In this example, we introduce a Discount class and extend it using inheritance instead of modifying the original class.

</>
Copy
class Discount:
    def apply_discount(self, price):
        return price


class SeasonalDiscount(Discount):
    def apply_discount(self, price):
        return price * 0.9  # 10% discount


class ClearanceDiscount(Discount):
    def apply_discount(self, price):
        return price * 0.7  # 30% discount


# Applying different discounts
original_price = 100

seasonal = SeasonalDiscount()
print("Seasonal Discount Price:", seasonal.apply_discount(original_price))

clearance = ClearanceDiscount()
print("Clearance Discount Price:", clearance.apply_discount(original_price))

Here, the base Discount class is extended by SeasonalDiscount and ClearanceDiscount without modifying the original implementation.

Output:

Seasonal Discount Price: 90.0
Clearance Discount Price: 70.0

3. Liskov Substitution Principle (LSP)

The Liskov Substitution Principle states that a subclass should be able to replace its superclass without affecting the behavior of the program.

In this example, we create a base class Bird and its subclasses. The subclass Ostrich does not override fly() because it cannot fly, ensuring correct behavior.

</>
Copy
class Bird:
    def fly(self):
        return "I can fly!"


class Sparrow(Bird):
    pass  # Inherits fly behavior


class Ostrich(Bird):
    def fly(self):
        return "I cannot fly!"


# Using the classes
birds = [Sparrow(), Ostrich()]

for bird in birds:
    print(bird.fly())

Here, replacing a Bird instance with an Ostrich does not break the behavior of the program.

Output:

I can fly!
I cannot fly!

4. Interface Segregation Principle (ISP)

The Interface Segregation Principle states that a class should not be forced to implement methods it does not need.

In this example, we use separate interfaces for printers that only print and those that also scan.

</>
Copy
from abc import ABC, abstractmethod

class Printer(ABC):
    @abstractmethod
    def print_document(self):
        pass


class Scanner(ABC):
    @abstractmethod
    def scan_document(self):
        pass


class BasicPrinter(Printer):
    def print_document(self):
        return "Printing document..."


class MultiFunctionPrinter(Printer, Scanner):
    def print_document(self):
        return "Printing document..."

    def scan_document(self):
        return "Scanning document..."


# Using the classes
basic = BasicPrinter()
print(basic.print_document())

multi = MultiFunctionPrinter()
print(multi.print_document())
print(multi.scan_document())

Here, BasicPrinter only implements print_document(), while MultiFunctionPrinter implements both print_document() and scan_document().

Output:

Printing document...
Printing document...
Scanning document...

5. Dependency Inversion Principle (DIP)

The Dependency Inversion Principle states that high-level modules should depend on abstractions rather than concrete implementations.

</>
Copy
class EmailService:
    def send_email(self, message):
        print("Sending email:", message)


class Notification:
    def __init__(self, service):
        self.service = service

    def notify(self, message):
        self.service.send_email(message)


# Using the classes
email_service = EmailService()
notification = Notification(email_service)
notification.notify("Hello, SOLID!")

Here, Notification depends on an abstraction of an email service rather than a specific implementation.

Output:

Sending email: Hello, SOLID!