Understanding SOLID Principles with Python: A Beginner’s Guide

Manishankar Jaiswal
4 min readAug 1, 2024

--

Introduction

Welcome to the world of clean code! If you’re a new developer, you’ve probably heard about the importance of writing maintainable and scalable code. One way to achieve this is by following the SOLID principles. These five principles help you design your software in a way that’s easy to understand, modify, and extend.

SOLID DESIGN PRINCIPLE WITH PYTHON

In this blog post, we’ll break down each SOLID principle and show you how to apply them using Python with practical examples. Let’s dive in!

What are SOLID Principles?

SOLID is an acronym that stands for:

  1. Single Responsibility Principle (SRP)
  2. Open/Closed Principle (OCP)
  3. Liskov Substitution Principle (LSP)
  4. Interface Segregation Principle (ISP)
  5. Dependency Inversion Principle (DIP)

These principles were introduced by Robert C. Martin (Uncle Bob) and are fundamental to object-oriented design.

1. Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change, meaning it should have only one job or responsibility.

Example:

Let’s consider a simple example of a class that handles book information and its management:

class Book:
def __init__(self, title, author):
self.title = title
self.author = author

def get_book_info(self):
return f"{self.title} by {self.author}"

def save_book_to_file(self):
with open(f"{self.title}.txt", "w") as file:
file.write(self.get_book_info())

Here, the Book class has two responsibilities: handling book data and saving the book info to a file. To follow SRP, we should separate these responsibilities into different classes.

Refactored:

class Book:
def __init__(self, title, author):
self.title = title
self.author = author

def get_book_info(self):
return f"{self.title} by {self.author}"

class BookRepository:
def save_book_to_file(self, book):
with open(f"{book.title}.txt", "w") as file:
file.write(book.get_book_info())

Now, each class has a single responsibility.

2. Open/Closed Principle (OCP)

Definition: Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.

Example:

Let’s say we have a class that calculates the price of items with different discount strategies:

class PriceCalculator:
def calculate_price(self, item, discount_type):
if discount_type == "percentage":
return item.price * (1 - item.discount_rate)
elif discount_type == "fixed":
return item.price - item.discount_amount

If we want to add a new discount type, we need to modify the PriceCalculator class, which violates OCP.

Refactored:

from abc import ABC, abstractmethod

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

class PercentageDiscount(DiscountStrategy):
def __init__(self, discount_rate):
self.discount_rate = discount_rate

def calculate(self, price):
return price * (1 - self.discount_rate)

class FixedDiscount(DiscountStrategy):
def __init__(self, discount_amount):
self.discount_amount = discount_amount

def calculate(self, price):
return price - self.discount_amount

class PriceCalculator:
def calculate_price(self, item, discount_strategy):
return discount_strategy.calculate(item.price)

Now, to add a new discount type, we just need to create a new class that inherits from DiscountStrategy.

3. Liskov Substitution Principle (LSP)

Definition: Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.

Example:

Consider a class hierarchy for payment methods:

class Payment:
def pay(self, amount):
pass

class CreditCardPayment(Payment):
def pay(self, amount):
print(f"Paid {amount} using Credit Card")

class PayPalPayment(Payment):
def pay(self, amount):
print(f"Paid {amount} using PayPal")

class CashPayment(Payment):
def pay(self, amount):
raise Exception("Cash payments are not supported online")

Here, substituting CashPayment for Payment would break the program because CashPayment raises an exception.

Refactored:

class Payment(ABC):
@abstractmethod
def pay(self, amount):
pass

class CreditCardPayment(Payment):
def pay(self, amount):
print(f"Paid {amount} using Credit Card")

class PayPalPayment(Payment):
def pay(self, amount):
print(f"Paid {amount} using PayPal")

class OnlinePayment(Payment):
def pay(self, amount):
raise NotImplementedError("Online payments are not supported")

Now, all payment methods that can be used online will inherit from Payment, and those that cannot will inherit from OnlinePayment, ensuring they are not used incorrectly.

4. Interface Segregation Principle (ISP)

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

Example:

Imagine an interface for a device with multiple functionalities:

class MultifunctionDevice:
def print(self, document):
pass

def scan(self, document):
pass

def fax(self, document):
pass

A simple printer that only prints would have to implement unnecessary methods.

Refactored:

class Printer(ABC):
@abstractmethod
def print(self, document):
pass

class Scanner(ABC):
@abstractmethod
def scan(self, document):
pass

class Fax(ABC):
@abstractmethod
def fax(self, document):
pass

class SimplePrinter(Printer):
def print(self, document):
print("Printing document")

class MultiFunctionPrinter(Printer, Scanner, Fax):
def print(self, document):
print("Printing document")

def scan(self, document):
print("Scanning document")

def fax(self, document):
print("Faxing document")

Now, classes only implement the interfaces they need.

5. Dependency Inversion Principle (DIP)

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

Example:

Consider a class that sends notifications:

class EmailService:
def send_email(self, message):
print("Sending email:", message)

class Notification:
def __init__(self):
self.email_service = EmailService()

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

Here, Notification depends on the concrete EmailService class.

Refactored:

class MessageService(ABC):
@abstractmethod
def send_message(self, message):
pass

class EmailService(MessageService):
def send_message(self, message):
print("Sending email:", message)

class SMSService(MessageService):
def send_message(self, message):
print("Sending SMS:", message)

class Notification:
def __init__(self, message_service: MessageService):
self.message_service = message_service

def notify(self, message):
self.message_service.send_message(message)

Now, Notification depends on the MessageService abstraction, and we can easily switch to another service like SMSService.

Conclusion

Understanding and applying the SOLID principles can greatly improve the quality of your code. By following these principles, you can create software that’s easier to maintain, extend, and understand. Start incorporating these principles into your projects, and you’ll notice a significant improvement in your code quality.

Happy coding!

Feel free to leave any comments or questions below. If you found this post helpful, share it with your fellow developers!

--

--

Responses (1)