What Are the SOLID Principles?
SOLID is an acronym for five design principles that help developers write code that is easier to maintain, extend, and understand. Coined and popularized by Robert C. Martin (Uncle Bob), these principles apply primarily to object-oriented design but carry valuable lessons for any paradigm.
S — Single Responsibility Principle (SRP)
A class should have only one reason to change.
Every class or module should do exactly one thing. If a class handles both user authentication and email formatting, you have two reasons to change it — violating SRP.
# Bad: one class doing two jobs
class UserManager:
def save_user(self, user): ...
def send_welcome_email(self, user): ...
# Good: separate responsibilities
class UserRepository:
def save_user(self, user): ...
class EmailService:
def send_welcome_email(self, user): ...
Why it matters: Smaller, focused classes are easier to test, debug, and reuse.
O — Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification.
You should be able to add new behavior without changing existing code. Use interfaces or abstract base classes to define contracts, then extend via new implementations.
# Instead of modifying a calculate_discount() function for every new type,
# define a base class and extend it.
class Discount:
def apply(self, price): raise NotImplementedError
class SeasonalDiscount(Discount):
def apply(self, price): return price * 0.9
class LoyaltyDiscount(Discount):
def apply(self, price): return price * 0.85
L — Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types.
If Bird has a fly() method and Penguin extends Bird, calling fly() on a penguin breaks LSP. A subclass should honor the contract of its parent without unexpected behavior.
- Don't override methods to throw exceptions or do nothing.
- If a subclass can't fulfill the parent's contract, rethink the hierarchy.
I — Interface Segregation Principle (ISP)
No client should be forced to depend on methods it does not use.
Prefer many small, specific interfaces over one large "do-everything" interface. A Printable interface and a Scannable interface are better than one MultiFunctionDevice interface that forces simple printers to implement scanning.
D — Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
# Bad: high-level class directly depends on a concrete low-level class
class OrderProcessor:
def __init__(self):
self.db = MySQLDatabase() # Tightly coupled
# Good: depend on an abstraction
class OrderProcessor:
def __init__(self, database: Database):
self.db = database # Injected dependency
This makes code dramatically easier to test (inject a mock database) and swap implementations without touching business logic.
SOLID in Practice
| Principle | Core Idea | Key Benefit |
|---|---|---|
| SRP | One job per class | Easier to test and change |
| OCP | Extend, don't modify | Less regression risk |
| LSP | Subclasses honor parent contracts | Predictable polymorphism |
| ISP | Small focused interfaces | No unnecessary coupling |
| DIP | Depend on abstractions | Testable, flexible code |
Final Thought
You don't need to apply every SOLID principle perfectly in every file. Think of them as a compass, not a checklist. When your code becomes hard to change, test, or extend, revisit these principles — you'll often find a violation at the root of the problem.