SOLID Part 1 — SRP, OCP, LSP with Python Examples
Session 4 of the 48-session learning series.
Why this session matters
This is Session 04 of 48 in the OOP & Languages track. It builds on the rhythm of one focused topic, paced so you have time to actually absorb it rather than rush.
Agenda
- What SOLID is and why it's an 'engineer-grade' principle set
- SRP — Single Responsibility Principle, the 'reason to change' test
- OCP — Open/Closed Principle, via strategy + extension points
- LSP — Liskov Substitution, with the rectangle/square counter-example
- Where SOLID overreaches — pragmatic Python guidance
Pre-read (skim before the session)
- ArjanCodes — SOLID Principles in Python (video)
- Robert C. Martin — original SOLID essay collection
- Python's data model (dunder methods)
- Refactoring (Fowler) — table of contents
Deep dive
1. What SOLID is (and isn't)
SOLID is five OOP design principles formalised by Robert C. Martin in the early 2000s:
- S — Single Responsibility Principle
- O — Open/Closed Principle
- L — Liskov Substitution Principle
- I — Interface Segregation Principle (Session 11)
- D — Dependency Inversion Principle (Session 11)
They're heuristics, not laws. Applied well they make code testable and changeable; applied dogmatically they produce abstraction soup with 47 small files for a 200-line script. The skill is knowing which one is paying its way today.
2. SRP — Single Responsibility Principle
A class should have one reason to change.
The classic violation:
class Report:
def collect_data(self): ... # changes when data source changes
def calculate_metrics(self): ... # changes when business logic changes
def render_html(self): ... # changes when UI/template changes
def email_to_users(self): ... # changes when delivery mechanism changes
Four different stakeholders can each demand a change. Two of them changing at the same time → merge conflict + a bug surface that has nothing to do with their actual change.
Split:
class ReportCollector:
def collect(self) -> dict: ...
class ReportCalculator:
def compute(self, raw: dict) -> dict: ...
class HtmlReportRenderer:
def render(self, computed: dict) -> str: ...
class EmailDispatcher:
def send(self, html: str, recipients: list[str]) -> None: ...
Now each class has one reason to change, and you can unit-test each piece without spinning up SMTP.
Smell test: if a method's name says "and" — validate_and_save_user — you probably violated SRP.
3. OCP — Open/Closed Principle
Software entities should be open for extension, closed for modification.
You shouldn't have to modify a tested class to add a new behaviour — you should extend it. The mechanism is usually a strategy or template method.
Bad — adding a new payment method requires editing PaymentProcessor:
class PaymentProcessor:
def charge(self, method: str, amount: float):
if method == "card": self._charge_card(amount)
elif method == "upi": self._charge_upi(amount)
elif method == "paypal": self._charge_paypal(amount)
# adding a new method = editing this file, re-running tests, redeploying
Good — strategies:
from typing import Protocol
class PaymentStrategy(Protocol):
def charge(self, amount: float) -> None: ...
class CardPayment:
def charge(self, amount: float) -> None: ...
class UPIPayment:
def charge(self, amount: float) -> None: ...
class PaymentProcessor:
def __init__(self, strategy: PaymentStrategy):
self.strategy = strategy
def charge(self, amount: float) -> None:
self.strategy.charge(amount)
Adding PayPalPayment is now a new class, no edits to PaymentProcessor.
Python-specific note: Protocol (PEP 544) gives you structural subtyping, no need for inheritance.
4. LSP — Liskov Substitution Principle
Subtypes must be substitutable for their base types without changing program correctness.
The famous counter-example: rectangle and square.
class Rectangle:
def set_width(self, w): self.w = w
def set_height(self, h): self.h = h
def area(self): return self.w * self.h
class Square(Rectangle):
def set_width(self, w): self.w = w; self.h = w # mutates h too!
def set_height(self, h): self.w = h; self.h = h
A function that correctly assumes set_width doesn't affect height will break when passed a Square. Subtype broke a contract of the base.
Better — model what they share, not the inheritance you expect:
class Shape(Protocol):
def area(self) -> float: ...
@dataclass(frozen=True)
class Rectangle:
w: float; h: float
def area(self): return self.w * self.h
@dataclass(frozen=True)
class Square:
side: float
def area(self): return self.side ** 2
Both implement Shape; neither inherits from the other; no surprising state mutation.
LSP failure modes to watch for:
- Subclass throws an exception the base never threw.
- Subclass weakens preconditions or strengthens postconditions in a surprising way.
- Subclass overrides a method to do something semantically different (a
Bird.fly()overridden byPenguintoraise NotImplementedErroris the textbook fail).
5. Practical Python guidance
- Prefer composition over inheritance. Python has multiple inheritance, but
mixinchains are a debugging nightmare in production. Inject collaborators in__init__. - Use
Protocolfor interfaces. No need forABCin most cases; duck-typing is structurally enforced at type-check time. @dataclass(frozen=True)for value objects. Immutability eliminates a class of state-mutation bugs and makes LSP failures less likely.- Don't pre-abstract. Wait until you have two implementations of the same thing before you extract an interface. "Rule of three" applies.
6. Real example — refactoring a 400-line OrderService
Before:
class OrderService:
def place_order(self, user_id, items, payment_method):
# 1. validate user
# 2. validate items in stock
# 3. compute totals + tax
# 4. apply discount codes
# 5. process payment
# 6. create order record
# 7. update inventory
# 8. send confirmation email
# 9. publish OrderPlaced event
After (SRP + OCP + DIP — Session 11 covers DIP fully):
class OrderService:
def __init__(
self,
user_repo: UserRepo,
stock: StockChecker,
pricing: PricingEngine,
discounts: DiscountEngine,
payments: PaymentStrategy,
orders: OrderRepo,
events: EventBus,
notifier: Notifier,
): ...
def place_order(self, user_id, items, payment_method):
user = self.user_repo.get(user_id)
self.stock.reserve(items)
totals = self.pricing.compute(items, user)
totals = self.discounts.apply(totals, user)
self.payments.charge(user, totals.total)
order = self.orders.create(user, items, totals)
self.events.publish(OrderPlaced(order.id))
self.notifier.notify(user, order)
Each collaborator is testable in isolation. The order service is now an orchestrator, not a god class.
7. When SOLID overreaches
Three honest cases where applying SOLID makes things worse:
- One-off scripts. Don't extract
PaymentStrategyfrom a 30-line cron job. - Internal helpers under 100 lines. YAGNI beats OCP.
- Premature abstraction. Two implementations that might diverge → don't abstract yet.
The rule: SOLID earns its keep when a class has two real consumers and a history of changes.
8. What's next (Session 11 — SOLID Part 2)
- I — Interface Segregation
- D — Dependency Inversion
- Design patterns: Strategy (you saw it here), Factory, Observer
- When patterns help vs hurt
Reading material
Books:
- Clean Code — Robert C. Martin (ch. 3: Functions, ch. 10: Classes)
- Clean Architecture — Robert C. Martin (Part III: Design Principles — the SOLID chapters)
- Agile Software Development, Principles, Patterns, and Practices — Robert C. Martin (the book where SOLID was first laid out)
Papers / essays:
- The Principles of OOD (Uncle Bob, 2005) — the original SOLID writeup.
- Design Principles and Design Patterns (Robert C. Martin, 2000) — pre-SOLID summary essay.
Official docs:
- Python
abc— Abstract Base Classes — needed for OCP/DIP in Python. - Python
typing.Protocol(PEP 544) — structural subtyping, the Pythonic LSP.
Blog posts:
- SOLID Principles in Python — Real Python — best modern Python walkthrough.
- The SOLID Principles — Khalil Stemmler — clearest TS examples; concepts transfer cleanly to Python.
- SOLID Principles: When to use them — Codecademy — short overview with code.
In-depth research material
- python-patterns — github.com/faif/python-patterns — ~41k ★, every Gang-of-Four pattern in idiomatic Python.
- refactoring.guru — Design Patterns — interactive UML + code samples in many languages.
- Refactoring (Martin Fowler, 2nd ed.) — book site — the SOLID complement: how to get to clean designs.
- Stack Overflow — SOLID criticism threads — read the contrary takes (e.g. Dan North on "why every elegant SRP becomes a god-class facade").
- Dan North — Why every element of SOLID is wrong — the famous contrarian deck; sharpens your own view.
- Programming Throwdown #102 — Clean Code — episode-length discussion of how SOLID lands in real codebases.
Videos
- The S.O.L.I.D. Principles of OO & Agile Design — Uncle Bob Martin — TDD TV · 1 h 12 min — Robert C. Martin's own conference talk; canonical source.
- Single Responsibility Principle (SOLID) — Code Walks 006 — Christopher Okhravi · 6 min — bite-size; first in a great per-principle series.
- Liskov Substitution Principle (SOLID) — Christopher Okhravi · 16 min — the trickiest principle, explained with the bird/penguin example everyone needs.
- Depend on Abstractions not Concretions (DIP) — Christopher Okhravi · 12 min — the DIP refresher; how to wire a real app's dependencies.
- SOLID Principles: Do You Really Understand Them? — Alex Hyett · 7 min — modern, concise re-cap with concrete code; great as a closer.
LeetCode — Design Parking System
- Link: https://leetcode.com/problems/design-parking-system/
- Difficulty: Easy
- Why this problem: Classic OOP modelling — counters per slot type, decrement on park.
- Time-box: 30 minutes. Look up the editorial only after.
Post-session checklist
By the end of this session you should be able to:
- Apply the 'reason to change' test to a real class in your repo.
- Refactor an if/elif chain over a string type into a strategy with Protocol.
- Spot a LSP violation in a subclass (or argue none exist).
- Argue when not to apply SOLID and why YAGNI wins.
- Solve
design-parking-systemby modelling levels + slot types cleanly.
Generated from sessions_data.py + content_part*.py. To edit a video / leetcode / title, edit the data file and re-run write_sessions.py.