Building Robust, Maintainable Python Code with SOLID Principles
Master SOLID principles to write cleaner, scalable, and maintainable Python applications
Introduction
In modern software development, writing maintainable and scalable code is critical. The SOLID principles, originally introduced by Robert C. Martin, provide a structured approach to designing clean, extensible, and flexible software.
Although Python is dynamically typed and encourages duck typing, applying SOLID principles helps prevent code smells, reduce dependencies, and improve testability.
In this article, we’ll explore each SOLID principle with Python examples and best practices.
Understanding SOLID Principles
The SOLID acronym 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)
Let’s break them down with practical Python examples.
1️⃣ Single Responsibility Principle (SRP)
💡 A class should have only one reason to change.
Bad Example:
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 f:
f.write(self.generate_report())
📌 Issue: This class handles both report generation and file operations, violating SRP.
Better Approach:
class Report:
def __init__(self, data):
self.data = data
def generate(self) -> str:
return f"Report Data: {self.data}"
class FileHandler:
@staticmethod
def save(filename: str, content: str):
with open(filename, "w") as f:
f.write(content)
report = Report("Sales Data")
FileHandler.save("report.txt", report.generate())
✅ SRP Applied: The Report class focuses on generating reports, while FileHandler deals with file operations.
2️⃣ Open/Closed Principle (OCP)
💡 Software entities should be open for extension but closed for modification.
Bad Example:
class Discount:
def apply_discount(self, price, discount_type):
if discount_type == "percentage":
return price * 0.9
elif discount_type == "fixed":
return price - 10
else:
return price
📌 Issue: Every new discount type requires modifying this class, violating OCP.
Better Approach (Using Polymorphism):
from abc import ABC, abstractmethod
class Discount(ABC):
@abstractmethod
def apply(self, price: float) -> float:
pass
class PercentageDiscount(Discount):
def apply(self, price: float) -> float:
return price * 0.9
class FixedDiscount(Discount):
def apply(self, price: float) -> float:
return price - 10
def apply_discount(price: float, discount: Discount) -> float:
return discount.apply(price)
print(apply_discount(100, PercentageDiscount())) # 90.0
print(apply_discount(100, FixedDiscount())) # 90.0
✅ OCP Applied: New discount types can be added without modifying existing code.
3️⃣ Liskov Substitution Principle (LSP)
💡 Subtypes must be substitutable for their base types without altering program correctness.
Bad Example:
class Bird:
def fly(self):
return "Flying"
class Penguin(Bird):
def fly(self):
raise NotImplementedError("Penguins can't fly")
📌 Issue: The subclass violates the contract of its parent, breaking LSP.
Better Approach:
from abc import ABC, abstractmethod
class Bird(ABC):
@abstractmethod
def move(self):
pass
class FlyingBird(Bird):
def move(self):
return "Flying"
class NonFlyingBird(Bird):
def move(self):
return "Walking"
class Sparrow(FlyingBird):
pass
class Penguin(NonFlyingBird):
pass
print(Sparrow().move()) # "Flying"
print(Penguin().move()) # "Walking"
✅ LSP Applied: Subtypes correctly extend the base class without breaking expectations.
4️⃣ Interface Segregation Principle (ISP)
💡 Clients should not be forced to depend on interfaces they do not use.
Bad Example:
class Worker:
def work(self):
pass
def eat(self):
pass
class Robot(Worker):
def work(self):
return "Working"
def eat(self):
raise NotImplementedError("Robots don't eat")
📌 Issue: The Robot
class doesn’t need the eat()
method.
Better Approach:
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):
return "Working"
def eat(self):
return "Eating"
class Robot(Workable):
def work(self):
return "Working"
✅ ISP Applied: Separated concerns using multiple interfaces.
5️⃣ Dependency Inversion Principle (DIP)
💡 High-level modules should not depend on low-level modules. Both should depend on abstractions.
Bad Example:
class MySQLDatabase:
def connect(self):
return "Connected to MySQL"
class App:
def __init__(self):
self.db = MySQLDatabase() # Tight coupling
def fetch_data(self):
return self.db.connect()
📌 Issue: The App
class is tightly coupled to MySQLDatabase
.
Better Approach (Using Dependency Injection):
class Database(ABC):
@abstractmethod
def connect(self):
pass
class MySQLDatabase(Database):
def connect(self):
return "Connected to MySQL"
class App:
def __init__(self, db: Database):
self.db = db
def fetch_data(self):
return self.db.connect()
app = App(MySQLDatabase())
print(app.fetch_data())
✅ DIP Applied: App
depends on an abstraction, not a concrete class.
Conclusion
Applying SOLID principles in Python enhances code quality, scalability, and maintainability. Whether building microservices, APIs, or enterprise applications, structuring your code using these principles prevents technical debt and ensures long-term success.
🚀 Next Steps: Try applying SOLID principles in your Python projects today!