Last lesson ended with one question you should ask about every class: "what job does this class own?" SOLID is that same instinct, sharpened into five separate questions — one for responsibility, one for extension, one for substitution, one for interfaces, and one for dependencies. Learn them as questions, not definitions, and you'll actually use them at a whiteboard.
S — Single Responsibility Principle
Take this class, exactly as a junior developer might first write it:
class UserService:
def register(self, user): ...
def send_welcome_email(self, user): ...
def generate_invoice(self, user): ...
def log_analytics_event(self, event): ...Now imagine the email templating library changes, or marketing wants a new analytics field. None of that is about registration — yet every change means editing, re-testing, and re-deploying UserService.
Ask: "if one requirement changes, how many unrelated things will this class force me to touch?" If the answer is more than one, you've found an SRP violation. More formally: a class should have one, and only one, reason to change — the definition Robert C. Martin ("Uncle Bob") gives for SRP.
Split it along those reasons, and each piece gets simpler:
class UserService:
def register(self, user): ...
class NotificationService:
def send_welcome_email(self, user): ...
class InvoiceGenerator:
def generate_invoice(self, user): ...
class AnalyticsLogger:
def log_event(self, event): ...O — Open/Closed Principle
A payment system with a growing chain, six months into a project:
def calculate_discount(order, discount_type):
if discount_type == "FESTIVE":
return order.total * 0.10
elif discount_type == "STUDENT":
return order.total * 0.05
elif discount_type == "LOYALTY":
... # a new elif every time marketing invents a new discountEvery new discount type means editing this same function — code that already works, is already tested, and now has to be re-tested because you touched it. Ask: "if I add a new type, do I keep modifying existing code?" If yes, that's the Open/Closed pressure point: modules should be open for extension, but closed for modification.
The fix is to depend on an abstraction instead of a growing conditional — a new discount becomes a new class, and calculate_discount never changes again:
class DiscountStrategy:
def calculate(self, order): ...
class FestiveDiscount(DiscountStrategy):
def calculate(self, order): return order.total * 0.10
class StudentDiscount(DiscountStrategy):
def calculate(self, order): return order.total * 0.05
# adding LoyaltyDiscount later = one new class, zero edits aboveYou will meet this exact shape again very soon — it's the Strategy pattern, and OCP is the reason it exists.
L — Liskov Substitution Principle
Barbara Liskov's idea, in plain words: if code works with a base class, it should keep working when you hand it any subclass instead — without surprises.
The classic trap:
class Rectangle:
def set_width(self, w): self.width = w
def set_height(self, h): self.height = h
class Square(Rectangle):
def set_width(self, w):
self.width = w
self.height = w # keeps it a square... but breaks the caller
def set_height(self, h):
self.width = h
self.height = hMathematically, a square is a rectangle. But behaviourally, code that does rect.set_width(5); rect.set_height(10) expecting a 5×10 rectangle silently gets a 10×10 square instead when rect is actually a Square. Ask: "can I replace the parent with this child without surprising the caller?" Here, no — so Square should not inherit from Rectangle at all. This is why "is-a" in English is not the same as "is-a" in your class hierarchy — only use inheritance when the subtype can honestly stand in for the parent everywhere.
I — Interface Segregation Principle
A fat interface, forced onto a class that can't use all of it:
class Worker:
def work(self): ...
def eat(self): ...
class RobotWorker(Worker):
def work(self): ...
def eat(self):
raise NotImplementedError # a robot doesn't eat!Ask: "is this class being forced to depend on methods it doesn't need?" RobotWorker is forced to have an eat() method it can never honestly implement. The fix: split the fat interface into smaller, role-specific ones.
class Workable:
def work(self): ...
class Eatable:
def eat(self): ...
class HumanWorker(Workable, Eatable):
def work(self): ...
def eat(self): ...
class RobotWorker(Workable):
def work(self): ... # no forced eat()Prefer many small, specific interfaces over one large one — no client should be forced to depend on methods it does not use.
D — Dependency Inversion Principle
Business logic reaching directly for a concrete database class:
class OrderService:
def __init__(self):
self.repo = MySQLOrderRepository() # concrete, low-level detail
def place_order(self, order):
self.repo.save(order)Ask: "if I replace this implementation, how many classes need modification?" Here, swapping to PostgreSQL — or injecting a fake repository in a unit test — means editing OrderService itself. Depend on an abstraction instead, and hand in the concrete implementation from outside:
class OrderRepository: # abstraction
def save(self, order): ...
class MySQLOrderRepository(OrderRepository):
def save(self, order): ... # concrete detail, plugged in later
class OrderService:
def __init__(self, repo: OrderRepository): # depends on the abstraction
self.repo = repo
def place_order(self, order):
self.repo.save(order)High-level modules (business logic) shouldn't depend on low-level modules (database details) — both should depend on abstractions. This single change is also what makes fast, isolated unit testing possible: a test can hand OrderService a fake in-memory repository instead of a real database.
SOLID as five questions, together
| Letter | Ask your design |
|---|---|
| S | If one requirement changes, how many unrelated things does this class force me to touch? |
| O | If I add a new type, do I keep modifying existing code? |
| L | Can I replace the parent with this child without surprising the caller? |
| I | Is this class being forced to depend on methods it doesn't need? |
| D | If I replace this implementation, how many classes need modification? |
Notice none of these questions mention a pattern by name. That's deliberate — SOLID is the reasoning; patterns (starting next lesson) are simply named structures that satisfy one or more of these questions particularly well.
Common mistakes
- Reciting the five definitions from memory without being able to spot a violation in unfamiliar code.
- Treating LSP as "does the subclass make sense in English" instead of "does it behave safely wherever the parent is used."
- Applying OCP everywhere pre-emptively — adding interfaces for types that will realistically never have a second variant.
- Confusing ISP's question (forced to depend on unused methods) with DIP's question (forced to depend on a concrete implementation) — they sound similar but target different problems.
Quick recap
| Principle | One-liner |
|---|---|
| SRP | One class, one reason to change. |
| OCP | Add new behaviour with new code, not edits to old code. |
| LSP | A subclass must behave safely wherever the parent is expected. |
| ISP | Small, role-specific interfaces beat one fat interface. |
| DIP | Depend on abstractions, not concrete low-level details. |
One thing to remember
SOLID isn't five things to memorise — it's five questions you keep asking your own design. Every pattern in the rest of this course exists because it answers one of these five questions well.
Practice Zone
Six MCQs — one per principle, plus one that mixes them up on purpose — then three reasoning questions.
A UserService class registers users, sends welcome emails, generates invoices, and logs analytics events. Which principle does this violate?
Asked in


Every time a new discount type is added, a developer edits a large if/else chain inside calculateDiscount(). What does this violate, and what's the fix direction?
Asked in

Square inherits from Rectangle and overrides setWidth() to also change the height (to keep it a square). What goes wrong?
Asked in

An interface Worker has work() and eat(). A RobotWorker class is forced to implement eat() with an empty or throwing body. What's the fix?
Asked in

OrderService directly creates new MySQLOrderRepository() inside its constructor. Why is this a DIP problem?
Asked in


Which pairing of SOLID principle to its one-line interview question is WRONG?
Asked in

Think it through, then reveal:
Isn't splitting one class into five smaller classes just adding complexity? Why is that considered better design?
Asked in

How does the Strategy pattern (covered in the next lessons) relate to the Open/Closed Principle?
Asked in

Why does Dependency Inversion make unit testing easier?
Asked in

FAQ
Do I need to apply all five SOLID principles to every class I write?
No — SOLID is a set of questions to check a design against, not a checklist every class must satisfy by force. A tiny, stable class with one obvious job doesn't need to be "made more SOLID." Apply them where a real pain (a God class, a growing if/else, a hard-to-test dependency) actually shows up.
Which SOLID principle comes up most in interviews?
SRP and OCP, by far — most "what would you change if I added X" follow-ups are really testing whether your design already respects Open/Closed. LSP is asked more conceptually (spot the bug in this inheritance), and ISP/DIP show up most when discussing testability or a data-access layer.
Is dependency injection the same as Dependency Inversion?
Related but not identical. Dependency Inversion is the principle — depend on abstractions, not details. Dependency Injection is a technique for satisfying it — handing a class its dependencies (often through the constructor) instead of letting it construct them itself. DI is how you make code follow DIP in practice.
SOLID told you when an abstraction is justified. Now let's meet the first named pattern that uses one — Lesson 3: Singleton Pattern →


