What you'll learn
Quick Answer
Single responsibility: one reason to change. Open/closed: extend without editing. Liskov: subclasses must be usable as the parent. Interface segregation: no forced unused methods. Dependency inversion: depend on abstractions, not concrete classes.
S — Single Responsibility
A class should have one reason to change.
class Report:
def fetch_data(self): ... # database changes
def format_html(self): ... # design changes
def send_email(self): ... # mail provider changes
Three unrelated reasons to edit this class. A change of mail provider risks breaking report formatting, because they live together and share state. In a team, three people end up editing the same file for unrelated reasons.
Split into ReportRepository, ReportFormatter and EmailSender. Each has one reason to change, each is testable alone.
The common misreading is "a class should do one thing", which leads to hundreds of one-method classes. The principle is about reasons to change — things that change together belong together.
O — Open/Closed
Open for extension, closed for modification.
def area(shape):
if shape.type == "circle": return 3.14 * shape.r ** 2
elif shape.type == "square": return shape.side ** 2
# every new shape edits this function
Each new shape means editing tested, working code — and risking the shapes that already worked.
class Circle:
def area(self): return 3.14 * self.r ** 2
class Square:
def area(self): return self.side ** 2
total = sum(s.area() for s in shapes) # never changes
Adding a triangle now means adding a class, not editing existing ones.
Applied too eagerly this produces abstraction for variation that never arrives. A reasonable rule is to write the conditional first, and extract the abstraction the second or third time you edit it for the same reason.
L — Liskov Substitution
A subclass must be usable anywhere its parent is.
The classic violation:
class Bird:
def fly(self): ...
class Penguin(Bird):
def fly(self):
raise NotImplementedError("penguins cannot fly")
Any code accepting a Bird and calling fly() now breaks when handed a penguin. The inheritance says "a penguin is a bird", which is true biologically and false for this interface.
Practical smells that indicate a violation: a subclass overriding a method to throw, or to do nothing; and calling code checking isinstance before deciding what to call. If callers must know the concrete type, the abstraction is not working.
The fix is usually to reshape the hierarchy — separate Bird from FlyingBird — or to prefer composition over inheritance entirely.
I and D — Interface Segregation and Dependency Inversion
Interface Segregation: no client should be forced to depend on methods it does not use.
class Worker: # too broad
def work(self): ...
def eat(self): ...
class Robot(Worker):
def eat(self): pass # meaningless, but required
Empty implementations existing only to satisfy an interface mean the interface is doing too much. Split it into Workable and Feedable.
Dependency Inversion: depend on abstractions, not concrete implementations.
class OrderService:
def __init__(self):
self.db = MySQLDatabase() # locked to MySQL, untestable
class OrderService:
def __init__(self, db): # anything with the right methods
self.db = db
The second version can be given a real database in production and a fake in tests, without changing the class. This is the principle behind dependency injection in Spring and most modern frameworks — see Spring Boot basics.
Of the five, this one has the highest practical payoff, because it is what makes code testable.
An honest note on applying them
SOLID emerged from large object-oriented systems, and the principles are genuinely valuable there. Applied mechanically to small programs, they produce codebases where following a single request means opening nine files.
Three sensible guidelines:
- Duplication is cheaper than the wrong abstraction. Two similar functions are easy to change. One abstraction covering both badly is not.
- Wait for the second or third repetition before abstracting. The first occurrence does not tell you what varies.
- Optimise for reading. If a change requires opening many files to understand, the design is not helping regardless of which principles it satisfies.
For interviews, be able to give each principle with a one-line example. For real code, dependency inversion and single responsibility deliver most of the value; the rest matter as systems grow.
