
In Parts 42–47 you learned the four pillars — encapsulation, inheritance, polymorphism, and (in Part 47) abstraction. You now have every tool OOP offers. But here is the uncomfortable truth: you can use every tool correctly and still write code that is painful to change. Knowing the pillars is not the same as knowing how to arrange classes so a system survives years of new requirements.
SOLID is the missing layer. It is five design principles that turn "code that works today" into "code that keeps working as it grows."
This confuses almost everyone, so let us settle it before any code.
So the relationship is one-directional:
SOLID → depends on OOP (it uses classes, polymorphism, abstraction)
OOP → does NOT need SOLID to run
Analogy: OOP is knowing grammar and vocabulary. SOLID is knowing how to write clearly so someone else can read your paragraph and safely edit it. You can be grammatically perfect and still write an unreadable mess — that is OOP without SOLID.
Is OOP alone enough? No. OOP lets you create classes; it does not stop you from writing a 2,000-line "God class" that does everything, or a rigid inheritance tree that breaks every time you touch it. SOLID is the discipline that prevents exactly those mistakes.
Picture bad code as having four diseases: rigidity (hard to change), fragility (a change breaks unrelated things), immobility (nothing can be reused), and tight coupling (everything depends on everything). SOLID is the cure — each of the five principles is a targeted treatment for one of them.
The acronym comes from Robert C. Martin ("Uncle Bob") — a veteran engineer and co-author of the Agile Manifesto — who gathered these principles around 2000; Michael Feathers (author of Working Effectively with Legacy Code) later rearranged them into the memorable word SOLID:
| Letter | Principle |
|---|---|
| S | Single Responsibility Principle |
| O | Open/Closed Principle |
| L | Liskov Substitution Principle |
| I | Interface Segregation Principle |
| D | Dependency Inversion Principle |
Who came up with these? (worth a click)
- L — Liskov is named after Barbara Liskov, an MIT professor and winner of the 2008 Turing Award, who stated this rule in a landmark 1987 talk.
- What is the Turing Award? Computer science's highest honour — the "Nobel Prize of Computing" (a $1 million prize, awarded every year since 1966 by the ACM). It is named after Alan Turing (1912–1954), the father of computer science and AI — the man who cracked Nazi Germany's Enigma code in WWII and invented the Turing Machine, the theoretical blueprint behind every modern computer.
- O — Open/Closed was first described by Bertrand Meyer in 1988 (creator of the Eiffel language).
- S, I, D were formulated by Uncle Bob himself.
One cast, five lessons. Every principle below is shown with the same characters from Part 47 — the
Animalclasses with aspeak()method (Dog→ "Woof!",Cat→ "Meow!"). Each principle gets one tiny ❌ Without example and one ✅ With example, so you can see the exact line where the principle matters.
A class should have only one reason to change.
❌ Without SRP — one Dog doing two unrelated jobs (being a dog and talking to a database):
class Dog:
def speak(self):
return "Woof!"
def save_to_db(self): # why does a dog know about databases?
print("Saving dog to database")
Change the database → you edit the Dog. Change the sound → you edit the same class. Two reasons to change = two responsibilities.
✅ With SRP — one job per class:
class Dog:
def speak(self):
return "Woof!"
class AnimalRepository: # its only job is saving
def save(self, animal):
print("Saving animal to database")
Conclusion: one reason to change per class. If describing a class makes you say "and", split it.
Open for extension, closed for modification. Add new behaviour with new code, not by editing working code.
❌ Without OCP — every new animal forces you to reopen and edit make_sound():
class SoundMaker:
def make_sound(self, species):
if species == "dog":
return "Woof!"
elif species == "cat":
return "Meow!"
# new animal? edit this method AGAIN...
✅ With OCP — lean on the Animal abstraction; add a subclass, touch nothing old:
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def speak(self): ...
class Dog(Animal):
def speak(self): return "Woof!"
class Cow(Animal): # NEW animal — SoundMaker never changes
def speak(self): return "Moo!"
def make_sound(animal: Animal):
return animal.speak()
Conclusion: a growing if/elif chain is OCP asking for a new subclass.
A subclass must be usable anywhere its parent is expected — without surprising the caller. IS-A must be behaviourally true, not just structurally.
Who was Liskov? Barbara Liskov — MIT computer scientist and 2008 Turing Award winner. Her 1987 rule, in plain words: if
Sis a subtype ofT, you must be able to use anSwherever aTis expected — and nothing should break.
❌ Without LSP — Fish IS-A Animal on paper, but breaks the speak() promise:
class Animal:
def speak(self):
return "Some sound"
class Fish(Animal):
def speak(self):
raise NotImplementedError("Fish can't speak!") # 💥 any caller of .speak() crashes
✅ With LSP — only animals that can speak inherit speak():
class Animal:
def move(self):
return "Moving"
class SpeakingAnimal(Animal):
def speak(self):
return "Some sound"
class Dog(SpeakingAnimal): # speaks — safe to substitute
def speak(self): return "Woof!"
class Fish(Animal): # an Animal, but never promises speak()
def swim(self): return "Swimming"
Conclusion: if a subclass has to disable or throw on an inherited method, the IS-A is wrong. (Same trap as the textbook Square inheriting Rectangle.)
No class should be forced to implement methods it does not use. Prefer many small interfaces over one fat one.
❌ Without ISP — one fat Animal interface forces a Dog to fake fly():
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def speak(self): ...
@abstractmethod
def fly(self): ...
class Dog(Animal):
def speak(self): return "Woof!"
def fly(self):
raise NotImplementedError("Dogs can't fly") # forced, useless
✅ With ISP — small, capability-based interfaces; implement only what you can do:
class Speaker(ABC):
@abstractmethod
def speak(self): ...
class Flyer(ABC):
@abstractmethod
def fly(self): ...
class Dog(Speaker): # only speaks
def speak(self): return "Woof!"
class Duck(Speaker, Flyer): # speaks AND flies
def speak(self): return "Quack!"
def fly(self): return "Flying"
Conclusion: a fat interface makes classes lie with empty/throwing methods. Split by capability.
Depend on abstractions, not on concrete details. High-level code should not be nailed to a specific low-level class.
❌ Without DIP — AnimalShow builds its own Dog, so it is locked to dogs forever:
class Dog:
def speak(self): return "Woof!"
class AnimalShow:
def __init__(self):
self.performer = Dog() # can never be anything but a Dog
def start(self):
return self.performer.speak()
✅ With DIP — depend on Animal and inject the performer from outside:
# Animal, Dog, Cat as defined in the OCP example above
class AnimalShow:
def __init__(self, performer: Animal): # abstraction, injected
self.performer = performer
def start(self):
return self.performer.speak()
AnimalShow(Dog()).start() # Woof!
AnimalShow(Cat()).start() # Meow!
Passing the dependency in through __init__ is called dependency injection.
Conclusion: point dependencies at abstractions ("any object with speak()"), not concrete classes — that is how real systems swap Stripe for Razorpay without a rewrite.
| Principle | ❌ Without | ✅ With |
|---|---|---|
| S — SRP | Dog also saves to the database | Dog speaks · AnimalRepository saves |
| O — OCP | if/elif on species inside make_sound() | Animal ABC · add a subclass, edit nothing |
| L — LSP | Fish.speak() throws | only SpeakingAnimal has speak() |
| I — ISP | one fat Animal (speak + fly) | small Speaker / Flyer interfaces |
| D — DIP | AnimalShow builds Dog() inside | inject any Animal through __init__ |
Read the left column and you can feel the pain; read the right and you see the cure. That is the entire job of SOLID: isolate change so tomorrow's requirement touches one small class, not your whole system.
SOLID does not live alone. It is the backbone of a wider set of design wisdom you have already met:
| Rule | Where you saw it | Relationship |
|---|---|---|
| Composition over inheritance | Part 45 | Makes LSP and DIP practical |
| DRY (Don't Repeat Yourself) | throughout | SRP naturally removes duplication |
| Program to an interface | Part 47 (ABC/Protocol) | The heart of OCP and DIP |
| Encapsulate what varies | Part 44 | Isolate change → OCP |
SOLID is not the only principle, and design patterns are a different layer:
Singleton — guarantee exactly one instance ever (one config / DB connection).
class Database:
_instance = None
@classmethod
def get(cls):
if cls._instance is None: # first time → create it
cls._instance = Database()
return cls._instance # after that → same one
print(Database.get() is Database.get()) # True
Factory — one helper builds the right object for you.
def make_animal(species):
if species == "dog": return Dog()
if species == "cat": return Cat()
make_animal("dog").speak() # Woof!
Strategy — swap a behaviour at runtime by injecting a different object.
class AnimalShow:
def __init__(self, performer):
self.performer = performer
def start(self):
return self.performer.speak()
AnimalShow(Dog()).start() # Woof!
AnimalShow(Cat()).start() # Meow!
Observer — when one object changes, its subscribers are notified automatically.
class Zoo:
def __init__(self):
self.watchers = []
def add_animal(self, name):
for notify in self.watchers:
notify(name)
zoo = Zoo()
zoo.watchers.append(lambda n: print(f"New animal: {n}"))
zoo.add_animal("Dog") # New animal: Dog
Principles tell you what good design looks like; patterns are proven recipes that follow those principles. Both are language-agnostic — not Python-only.
Depends() is DIP as a language feature.Dog for Cat in AnimalShow.Model with .fit() / .predict() (scikit-learn's estimator interface). Swap models without rewriting the pipeline — OCP and LSP in action.Take the animal cast and fix one violation per principle. Keep each example tiny.
Dog that both speak()s and save_to_db()s. Split the saving into an AnimalRepository.make_sound() if/elif with an Animal ABC and Dog, Cat, Cow subclasses. Adding Cow must not edit any existing class.Fish(Animal) that breaks speak(), then fix it with a SpeakingAnimal layer so Fish never promises speak().Animal(speak, fly) that forces a Dog to fake fly(), then split into Speaker / Flyer; give a Duck both.AnimalShow that injects its performer, and run it once with a Dog and once with a Cat — changing only the object you pass in.Save as src/solid_zoo.py, with a short comment above each pair labelling it # without / # with.
Context Manager: Students Miss ಮಾಡುವ with Statement Secret | Python in Kannada | Part-51
Part 51
Context Manager: Students Miss ಮಾಡುವ with Statement Secret | Python in Kannada | Part-51
Part 51