System Design

SOLID Design Principles in Python

4 days, 19 hours ago ; F(visit_count) + Value(1) views
Share this

SOLID Design Principles in Python

 

The SOLID design principles are crucial in software development to write clean, maintainable & scalable code.

The Five key principles help developers create robust and flexible software architectures.

Robert C. Martin (Uncle Bob) introduced these principles, which are widely used in object-oriented programming.

The end goal is for you to understand how to write code that's easy to modify and extend without breaking existing functionality.

The SOLID Principles Explained with Python

1. Single Responsibility Principle (SRP)

A class should have only one reason to change.

A class should limit its responsibilities to a solitary aspect of the application function.

Bad Example:  Violates SRP
 

class Report:
    def __init__(self, data):
        self.data = data
    
    def generate_report(self):
        return f"Report Data: {self.data}"
    
    def save_to_file(self, filename):
        with open(filename, "w") as file:
            file.write(self.generate_report())

The Report class handles both report generation and file saving.

 It violates SRP because changes to the logic for saving the file affect that of generating the reports.

Good Example: Following SRP

class Report:
    def __init__(self, data):
        self.data = data
    
    def generate_report(self):
        return f"Report Data: {self.data}"

class FileSaver:
    def save_to_file(self, report, filename):
        with open(filename, "w") as file:
            file.write(report)

The Report class solely generates reports, while the FileSaver class saves the report to a file.

2. Open-Closed Principle (OCP)

A class should be extended but not modified.

Bad Example: Violates OCP

class Discount:
    def calculate(self, price, customer_type):
        if customer_type == "regular":
            return price * 0.9  # 10% discount
        elif customer_type == "vip":
            return price * 0.8  # 20% discount

Every time we introduce a new customer type, we have to modify the class.

As a result, we go against OCP.

Good Example: Follows OCP

from abc import ABC, abstractmethod

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

class RegularDiscount(DiscountStrategy):
    def apply_discount(self, price):
        return price * 0.9

class VIPDiscount(DiscountStrategy):
    def apply_discount(self, price):
        return price * 0.8

class PriceCalculator:
    def __init__(self, discount_strategy: DiscountStrategy):
        self.discount_strategy = discount_strategy
    
    def calculate(self, price):
        return self.discount_strategy.apply_discount(price)

We can add more discount strategies without modifications to the existing PriceCalculator class.

3. Liskov Substitution Principle (LSP)

Subtypes should be substitutable for their base types without altering correctness.

Bad Example:  Violates LSP

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

class Penguin(Bird):
    def fly(self):
        raise Exception("Penguins cannot fly!")

The Penguin class inherits from Bird but breaks expected behavior since penguins can't fly.

Good Example: Following LSP

class Bird:
    pass

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

class Penguin(Bird):
    def swim(self):
        print("Swimming")

We avoid forcing all birds to have a fly method.

4. Interface Segregation Principle (ISP)

A class should not be forced to implement interfaces it does not use.

Bad Example: Violating ISP

class Worker:
    def work(self): pass
    def eat(self): pass

What if we have a robot that works but does not eat?

Good Example: Follows ISP

from abc import ABC, abstractmethod

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

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

class Human(Workable, Eatable):
    def work(self):
        print("Working...")
    def eat(self):
        print("Eating...")

class Robot(Workable):
    def work(self):
        print("Working...")

We separate interfaces so that classes only implement what they need.

5. Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Instead, both should depend on abstractions.

Bad Example: Violates DIP

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

class UserService:
    def __init__(self):
        self.db = MySQLDatabase()  # Direct dependency

 If we change the database, we need to modify UserService.

Good Example: Follows DIP

from abc import ABC, abstractmethod

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

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

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

UserService now depends on an abstraction so we can switch databases easily.

Applying the SOLID Principles

  • Separate concerns in your classes.
  • Avoid modifying existing classes; extend them instead.
  • Ensure that subclasses follow the expected behavior of their parent classes.
  • Design small, focused interfaces.
  • Use dependency injection instead of hardcoding dependencies.

What's Next?

Start applying these SOLID principles in your Python projects. 

They make your code more readable, scalable, and easier to maintain. The next time you write or review code, ask yourself: Does it follow SOLID principles? If not, refactor!

Need more hands-on examples? Follow PythonHaven for more practical coding guides!

 

Become a member
Get the latest news right in your inbox. We never spam!

Read next

Summarized Key Software Design Principles

Summarized  Key Software Design Principles Here's a summarized table of the key software d… Read More

22 hours, 2 minutes ago . 8 views

Software Design Principles

22 hours, 49 minutes ago . 58 views