SOLID Principles
SOLID is an acronym for 5 design principles that help you write code that's easier to maintain, extend, and understand. These principles were popularized by Robert C. Martin (Uncle Bob).
---
S — Single Responsibility Principle
> "A class should have only one reason to change."
Each class should do one thing and do it well. If a class has multiple responsibilities, changes to one responsibility might break the other.
One class handles user data, sends emails, AND generates reports. Change email logic? Risk breaking reports.
Separate classes: UserRepository, EmailService, ReportGenerator. Each can change independently.
# ❌ This class does too many things
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def save_to_database(self):
# Database logic here
db.insert(self)
def send_welcome_email(self):
# Email logic here
smtp.send(self.email, "Welcome!")
def generate_report(self):
# Report logic here
return f"User Report: {self.name}"
Good Example:
# ✓ Each class has one responsibility
class User:
def __init__(self, name, email):
self.name = name
self.email = email
class UserRepository:
def save(self, user):
db.insert(user)
class EmailService:
def send_welcome(self, user):
smtp.send(user.email, "Welcome!")
class UserReportGenerator:
def generate(self, user):
return f"User Report: {user.name}"
---
O — Open/Closed Principle
> "Software entities should be open for extension, but closed for modification."
You should be able to add new functionality without changing existing code. This prevents breaking things that already work.
# ❌ Adding new shape requires modifying existing code
class AreaCalculator:
def calculate(self, shape):
if shape.type == "circle":
return 3.14 * shape.radius ** 2
elif shape.type == "rectangle":
return shape.width * shape.height
elif shape.type == "triangle": # Had to modify!
return 0.5 * shape.base * shape.height
Good Example:
# ✓ New shapes extend without modifying existing code
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
# Adding triangle? Just add a new class!
class Triangle(Shape):
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return 0.5 * self.base * self.height
---
L — Liskov Substitution Principle
> "Subtypes must be substitutable for their base types."
If you have a parent class, any child class should be able to replace it without breaking the program.
A Penguin is a Bird, but it can't fly. If your Bird class has a fly() method, Penguin violates LSP because calling penguin.fly() would fail or behave unexpectedly.
# ❌ Penguin can't properly substitute for Bird
class Bird:
def fly(self):
return "Flying high!"
class Penguin(Bird):
def fly(self):
raise Exception("Penguins can't fly!") # Breaks substitution!
def make_bird_fly(bird):
print(bird.fly()) # Crashes with Penguin!
Good Example:
# ✓ Proper hierarchy respects LSP
class Bird:
def move(self):
pass
class FlyingBird(Bird):
def move(self):
return "Flying!"
class SwimmingBird(Bird):
def move(self):
return "Swimming!"
class Eagle(FlyingBird):
pass
class Penguin(SwimmingBird):
pass
def make_bird_move(bird):
print(bird.move()) # Works for all birds!
---
I — Interface Segregation Principle
> "No client should be forced to depend on methods it doesn't use."
Don't create "fat" interfaces that force classes to implement methods they don't need. Split them into smaller, focused interfaces.
# ❌ Forces Robot to implement methods it can't use
class IWorker(ABC):
@abstractmethod
def work(self): pass
@abstractmethod
def eat(self): pass
@abstractmethod
def sleep(self): pass
class Robot(IWorker):
def work(self):
return "Working..."
def eat(self):
pass # Robots don't eat! Forced to implement.
def sleep(self):
pass # Robots don't sleep! Forced to implement.
Good Example:
# ✓ Small, focused interfaces
class Workable(ABC):
@abstractmethod
def work(self): pass
class Eatable(ABC):
@abstractmethod
def eat(self): pass
class Sleepable(ABC):
@abstractmethod
def sleep(self): pass
class Human(Workable, Eatable, Sleepable):
def work(self): return "Working..."
def eat(self): return "Eating lunch..."
def sleep(self): return "Sleeping..."
class Robot(Workable): # Only implements what it needs
def work(self): return "Working 24/7..."
---
D — Dependency Inversion Principle
> "High-level modules should not depend on low-level modules. Both should depend on abstractions."
Don't let your business logic depend directly on specific implementations (like MySQL or SendGrid). Depend on abstractions (interfaces) instead.
# ❌ High-level module depends on low-level implementation
class MySQLDatabase:
def save(self, data):
# MySQL-specific code
pass
class OrderService:
def __init__(self):
self.db = MySQLDatabase() # Tightly coupled!
def create_order(self, order):
self.db.save(order)
# Want to switch to PostgreSQL? Have to modify OrderService!
Good Example:
# ✓ Depend on abstraction, not implementation
class Database(ABC):
@abstractmethod
def save(self, data): pass
class MySQLDatabase(Database):
def save(self, data):
# MySQL-specific code
pass
class PostgreSQLDatabase(Database):
def save(self, data):
# PostgreSQL-specific code
pass
class OrderService:
def __init__(self, db: Database): # Depends on abstraction
self.db = db
def create_order(self, order):
self.db.save(order)
# Easy to switch databases!
order_service = OrderService(PostgreSQLDatabase())
---
SOLID Summary
One class = one job. Easier to understand and change.
Extend behavior without modifying existing code.
Child classes must work wherever parent is expected.
Many small interfaces > one fat interface.
Depend on abstractions, not concrete implementations.
When to Apply SOLID
- Production code
- Long-lived projects
- Code with many collaborators
- Code that needs to be testable
- Quick prototypes
- One-off scripts
- Very simple programs
- When it adds complexity without benefit
Next up: Design Patterns - reusable solutions to common problems that help you apply SOLID in practice.