As you develop software, a codebase that started out clean inevitably starts showing these symptoms:
- Fixing something in one place mysteriously breaks something elsewhere
- Adding new features becomes terrifying because it requires modifying existing code
- Code you wrote a few months ago is now unreadable
- A single class balloons to hundreds of lines, and nobody wants to touch it—the dreaded “God Class”
This isn’t a matter of individual skill—it’s a design problem. Without deliberate attention to code structure, projects inevitably grow complex and become unmaintainable.
The SOLID principles were formulated to systematically prevent these problems. SOLID is a set of five foundational object-oriented design principles, codified in the early 2000s by Robert C. Martin (a.k.a. Uncle Bob). Today, they serve as the design backbone across virtually every area of software development—web, AI/ML, gaming, embedded systems, and beyond.
This article explains each of the five SOLID principles with why it matters, bad-vs-good design comparisons, Python code examples, and real-world applications, all in a beginner-friendly format. While SOLID applies to any OOP language, all code examples use Python.
This article assumes you understand OOP basics (classes, inheritance, methods). If you’re starting from scratch with Python, check out the Python Getting Started Guide. For error handling design, see 7 Python Error Handling Patterns.
SOLID Principles at a Glance
| Principle | Full Name | In One Sentence |
|---|---|---|
| S | Single Responsibility Principle | One class, one job. Only one reason to change |
| O | Open/Closed Principle | Open for extension, closed for modification |
| L | Liskov Substitution Principle | Subtypes must be substitutable for their base types |
| I | Interface Segregation Principle | Don’t force clients to depend on methods they don’t use |
| D | Dependency Inversion Principle | Depend on abstractions, not concretions |
In a nutshell, SOLID is “a set of design rules for writing code that is resilient to change, hard to break, and easy to extend.”
Why Are SOLID Principles Necessary?
To understand why SOLID matters, let’s first look at code with a broken design.
class UserManager:
def register(self, name, email):
# User registration
pass
def send_welcome_email(self, email):
# Send email
pass
def save_to_database(self, user):
# Save to DB
pass
def generate_monthly_report(self):
# Generate report
pass
def validate_input(self, data):
# Validate input
pass
This class looks convenient at first, but it carries serious problems:
| Problem | Impact |
|---|---|
| 5+ responsibilities in one class | Any change requires touching this class |
| Wide blast radius for changes | Email changes could affect DB operations |
| Difficult to test | Testing email requires a DB connection |
| Not reusable | Can’t use email sending in another project without pulling everything |
| Team conflicts | Everyone edits the same file, causing merge conflicts |
This is a design where “one change can break everything”—and it gets worse as the project grows. SOLID is the design philosophy that prevents this.
| Before SOLID | After SOLID |
|---|---|
| No idea what a change will break | Impact is clear and changes are safe |
| Every feature addition modifies existing code | Extend without modifying existing code |
| Tests are impossible or painful | Each class can be tested independently |
| Code is unreadable | Each class has clear, focused responsibility |
S: Single Responsibility Principle (SRP)
A class should have only one reason to change.
The most fundamental and arguably the most important SOLID principle. “Responsibility” can be read as “reason to change.” If a class has multiple reasons to change, it has too many responsibilities.
Bad Example: Mixed Responsibilities
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def save(self):
# Save to DB (persistence responsibility)
pass
def send_email(self, subject, body):
# Send email (notification responsibility)
pass
def calculate_age(self, birthdate):
# Calculate age (business logic responsibility)
pass
Good Example: Separated Responsibilities
class User:
def __init__(self, name, email):
self.name = name
self.email = email
class UserRepository:
def save(self, user: User):
# Save to DB
pass
class EmailService:
def send(self, to: str, subject: str, body: str):
# Send email
pass
class AgeCalculator:
def calculate(self, birthdate) -> int:
# Calculate age
pass
| Class | Responsibility | Reason to Change |
|---|---|---|
| User | Hold user data | Only when data structure changes |
| UserRepository | Persistence (DB operations) | Only when DB schema or ORM changes |
| EmailService | Send emails | Only when email provider changes |
| AgeCalculator | Business logic | Only when calculation logic changes |
SRP does NOT mean “one class, one method.” It means “one reason to change.” A class can have multiple methods as long as they all serve the same responsibility.
Applying SRP too aggressively can lead to an explosion of tiny classes that are harder to manage than the original. Balance “one reason to change” with practical granularity.
O: Open/Closed Principle (OCP)
Software should be open for extension but closed for modification.
When adding new features, you should be able to extend without modifying existing code. Modifying existing code always risks introducing bugs. If you can extend without modification, the risk of breaking existing features drops to zero.
Bad Example: Extending via Conditionals
class DiscountCalculator:
def calculate(self, customer_type: str, price: float) -> float:
if customer_type == "normal":
return price * 0.9
elif customer_type == "vip":
return price * 0.8
elif customer_type == "super_vip":
return price * 0.7
# Must modify this method for every new customer type...
Good Example: Extending via Polymorphism
from abc import ABC, abstractmethod
class Discount(ABC):
@abstractmethod
def calculate(self, price: float) -> float:
pass
class NormalDiscount(Discount):
def calculate(self, price: float) -> float:
return price * 0.9
class VipDiscount(Discount):
def calculate(self, price: float) -> float:
return price * 0.8
# Adding a new discount type (no existing code modified)
class SuperVipDiscount(Discount):
def calculate(self, price: float) -> float:
return price * 0.7
Adding a new discount type just means creating a new class. The existing NormalDiscount and VipDiscount are never touched.
OCP is widely used in Python Web frameworks. Django’s middleware and Flask’s Blueprints are OCP-compliant designs that let you add functionality without modifying the framework itself.
L: Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types without breaking correctness.
Inheritance should only be used when a true “is-a” relationship exists. If a subclass violates the contract (behavioral promise) of its parent, client code will break unexpectedly.
Bad Example: Contract-Breaking Inheritance
class Bird:
def fly(self):
return "Flying!"
class Penguin(Bird):
def fly(self):
raise Exception("Penguins can't fly!")
# Client code breaks
def make_bird_fly(bird: Bird):
return bird.fly() # Throws exception if Penguin is passed
Good Example: Proper Inheritance Hierarchy
class Bird:
def eat(self):
return "Eating!"
class FlyingBird(Bird):
def fly(self):
return "Flying!"
class Penguin(Bird):
def swim(self):
return "Swimming!"
class Eagle(FlyingBird):
pass
# Safe: only FlyingBird is expected to fly
def make_bird_fly(bird: FlyingBird):
return bird.fly()
Directly mapping the mathematical fact “a square is a type of rectangle” into code causes an LSP violation. A Square class inheriting from Rectangle would break the rectangle’s contract that width and height can be changed independently. This is the famous “Square-Rectangle Problem.”
I: Interface Segregation Principle (ISP)
Clients should not be forced to depend on methods they don’t use.
Instead of one large interface, split it into smaller interfaces that each client actually needs.
Bad Example: Fat Interface
from abc import ABC, abstractmethod
class Worker(ABC):
@abstractmethod
def work(self):
pass
@abstractmethod
def eat(self):
pass
@abstractmethod
def sleep(self):
pass
class Robot(Worker):
def work(self):
return "Working!"
def eat(self):
raise NotImplementedError("Robots don't eat")
def sleep(self):
raise NotImplementedError("Robots don't sleep")
Good Example: Segregated Interfaces
from abc import ABC, abstractmethod
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!"
def sleep(self):
return "Sleeping!"
class Robot(Workable):
def work(self):
return "Working!"
In Python, ISP is typically achieved through multiple inheritance (Mixin pattern). Java and C# use explicit interface keywords, but Python combines ABCs (Abstract Base Classes) to achieve the same effect.
D: Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Rather than depending directly on concrete implementations (MySQL, SendGrid, etc.), depend on abstractions (interfaces) to make swapping implementations easy.
Bad Example: Direct Dependency on Concretions
class MySQLDatabase:
def save(self, data):
# Save to MySQL
pass
class UserService:
def __init__(self):
self.db = MySQLDatabase() # Directly depends on MySQL
def create_user(self, name):
self.db.save({"name": name})
Good Example: Depending on Abstractions + DI
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
def save(self, data):
pass
class MySQLDatabase(Database):
def save(self, data):
# Save to MySQL
pass
class PostgreSQLDatabase(Database):
def save(self, data):
# Save to PostgreSQL
pass
class UserService:
def __init__(self, db: Database): # Depends on abstraction
self.db = db
def create_user(self, name):
self.db.save({"name": name})
# Usage
service = UserService(MySQLDatabase())
# Easy to swap
service = UserService(PostgreSQLDatabase())
DIP almost always goes hand-in-hand with Dependency Injection (DI). DI means injecting dependencies from the outside rather than creating them internally. In Python, the simplest form of DI is passing dependencies through __init__ arguments.
Over-applying DIP by creating abstract interfaces for everything is over-engineering. Don’t add abstractions for things unlikely to change (utility functions, etc.). Ask: “Is this likely to be swapped in the future?”
What Happens When You Follow SOLID — Before / After
| Aspect | Without SOLID | With SOLID |
|---|---|---|
| Change safety | No idea what a change will break | Impact is clear and changes are safe |
| Adding features | Modify existing code every time | Just add new classes |
| Testing | Too many dependencies to test | Each class testable in isolation |
| Readability | God Classes make understanding impossible | Small classes with clear responsibilities |
| Team development | Everyone edits the same files | Separated concerns reduce conflicts |
| Long-term maintenance | Technical debt accumulates | Sustainable structure maintained |
SOLID in Practice
| Technology / Architecture | Related SOLID Principles | Example |
|---|---|---|
| Web Frameworks | OCP, DIP | Django middleware, FastAPI dependency injection |
| Microservices | SRP | Each service owns a single responsibility |
| Clean Architecture | All principles | Layer separation and dependency direction control |
| DDD (Domain-Driven Design) | SRP, DIP | Separating domain models from infrastructure |
| AI/ML Pipelines | SRP, OCP | Separating preprocessing, model, and postprocessing |
| API Design | ISP | Exposing only the minimal necessary endpoints |
In Python Web framework-based API development, separating routing, business logic, and data access (SRP), adding features via middleware (OCP), and abstracting the DB for testability (DIP) are all SOLID in action. The design patterns in Python Security Implementation Patterns—separating input validation, authentication, and authorization—are SRP in practice.
Learning Priority for Beginners
| Priority | Principle | Why |
|---|---|---|
| 1 (Top) | S — Single Responsibility | Most intuitive with immediate impact. This alone dramatically improves design |
| 2 | O — Open/Closed | Prevents conditional explosion; teaches safe extension |
| 3 | D — Dependency Inversion | Foundation for testable design. DI pattern is essential in practice |
| 4 | I — Interface Segregation | Shines in large projects. Be aware at medium+ scale |
| 5 | L — Liskov Substitution | Important when inheritance is heavy. OK to skip initially |
Start by focusing on SRP alone. Just asking “Does this class have only one reason to change?” will dramatically improve your code quality. The other principles will become naturally relevant as you gain experience.
Common Misconceptions
| Misconception | Reality |
|---|---|
| You must follow all SOLID principles | Awareness matters most. Adjust application based on context |
| Apply perfectly from the start | Start with SRP only. Apply others incrementally |
| SOLID eliminates all bugs | It improves design; logic bugs are a separate issue |
| SOLID is only for OOP | Similar thinking applies to functional programming too |
| Not needed for small projects | Small projects benefit most from forward-looking design |
Being too rigid about “following SOLID” can make simple code unnecessarily complex. SOLID is a means, not an end. The goal is “code that’s easy to change and hard to break”—use SOLID as a guiding framework for that goal.
What to Learn After SOLID
| Topic | Related Principles | Overview |
|---|---|---|
| Design Patterns | OCP, DIP | GoF 23 Patterns: Strategy, Observer, Factory, etc. |
| Clean Architecture | All principles | Architecture design extending SOLID |
| Dependency Injection | DIP | Concrete patterns for implementing Dependency Inversion |
| DDD (Domain-Driven Design) | SRP, DIP | Design methodology centered on business logic |
| Refactoring | All principles | Techniques for improving existing code toward SOLID compliance |
FAQ
Q: Should beginners learn SOLID?
Yes—the earlier, the better. Design habits are extremely hard to fix later. Just being aware of “one reason to change” from the start makes a huge difference in long-term growth.
Q: Is SOLID needed for Python?
Absolutely. SOLID is a design philosophy, not language-specific. It applies to Python, Java, C++, TypeScript, Go, and any OOP-supporting language. Python’s dynamic typing arguably makes good design more important.
Q: Must I follow all five?
Start with SRP alone. Just that one principle dramatically improves code quality. The others will naturally become relevant as you gain experience.
Q: When should I think about SOLID?
When designing classes and modules. Especially when creating a new class, when an existing class grows too large, or when tests become hard to write.
Q: Is SOLID hard?
It may feel abstract at first, but it becomes natural as you practice. SRP and OCP in particular have many “aha” moments—you’ll soon write SOLID-compliant code without consciously thinking about it.
Summary
SOLID is a set of five design principles for writing code that is resilient to change, hard to break, and easy to extend.
- S (Single Responsibility): One class, one job. Only one reason to change
- O (Open/Closed): Extend without modifying existing code
- L (Liskov Substitution): Subtypes must honor their parent’s contract
- I (Interface Segregation): Don’t force unnecessary methods
- D (Dependency Inversion): Depend on abstractions, not concretions
SOLID is not something to memorize—it’s a design philosophy. Understand why it exists, and it will naturally reflect in your code. Start by focusing on the Single Responsibility Principle alone. That one change will dramatically improve your code quality.
Related articles: Python Getting Started Guide / 7 Python Error Handling Patterns / Python Web Framework Comparison / Python Security Implementation Patterns / Programming Language Comparison Guide

Leave a Reply