Introduction
The Liskov Substitution Principle (LSP) and Interface Segregation Principle (ISP) both address how abstractions should be designed and used. LSP ensures that a subclass can stand in for its base class without surprising callers, while ISP ensures that interfaces stay small enough that clients only depend on what they actually use.
Cricket analogy: LSP is like ensuring any specialist opener can be swapped into the top order without surprising the team's run-rate plan, while ISP ensures a bowling coach's checklist only asks about bowling skills, not batting stats an all-rounder doesn't need.
Explanation
LSP, formulated by Barbara Liskov, states that if S is a subtype of T, objects of type T in a program should be replaceable with objects of type S without altering the correctness of that program. In practice this means subclasses must not strengthen preconditions, weaken postconditions, or throw away behavior that callers rely on. The classic counterexample is modeling a Square as a subclass of Rectangle: a Rectangle's contract allows width and height to be set independently, but a Square must keep both equal, so setting one silently changes the other and breaks code written against Rectangle's contract.
Cricket analogy: Just as a genuine all-rounder must bowl and bat without one skill secretly breaking the other, LSP says a substitute must fully honor the original player's role; the classic trap is assuming a specialist 'floater' batsman fits any batting-order slot when the role's contract (fixed position) actually forbids it.
ISP states that no client should be forced to depend on methods it does not use. A single 'fat' interface that bundles many unrelated operations forces every implementer to provide (or stub out) methods irrelevant to it. The fix is to split the fat interface into several smaller, role-specific interfaces so classes implement only the interfaces that describe what they actually do.
Cricket analogy: ISP is like not forcing a specialist wicketkeeper to also be evaluated on fast-bowling metrics; instead, the keeper's fitness assessment interface should be split so only relevant skills (glovework, reflexes) are required, not pace-bowling speed radar readings.
Example
# --- LSP: classic Rectangle/Square counterexample ---
class Rectangle:
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def set_width(self, width: float) -> None:
self.width = width
def set_height(self, height: float) -> None:
self.height = height
def area(self) -> float:
return self.width * self.height
class Square(Rectangle):
"""Violates LSP: forces width == height, breaking Rectangle's contract."""
def set_width(self, width: float) -> None:
self.width = width
self.height = width # silently changes height too!
def set_height(self, height: float) -> None:
self.width = height
self.height = height # silently changes width too!
def resize_and_check(rect: Rectangle) -> None:
rect.set_width(5)
rect.set_height(10)
# A caller relying on Rectangle's contract expects area == 50 here.
assert rect.area() == 50, f"LSP broken: got {rect.area()}"
# resize_and_check(Rectangle(1, 1)) # passes
# resize_and_check(Square(1, 1)) # fails: area is 100, not 50
# LSP-compliant fix: don't force Square to inherit Rectangle's mutable contract.
# Instead, use a shared read-only Shape abstraction.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
...
class FixedRectangle(Shape):
def __init__(self, width: float, height: float):
self._width = width
self._height = height
def area(self) -> float:
return self._width * self._height
class FixedSquare(Shape):
def __init__(self, side: float):
self._side = side
def area(self) -> float:
return self._side * self._side
# --- ISP: splitting a fat interface into focused ones ---
# Before: one bloated interface forces every worker to implement everything.
class FatWorker(ABC):
@abstractmethod
def work(self): ...
@abstractmethod
def eat(self): ...
@abstractmethod
def sleep(self): ...
class RobotWorker(FatWorker):
def work(self):
print("welding")
def eat(self):
raise NotImplementedError("robots don't eat") # forced, unused method
def sleep(self):
raise NotImplementedError("robots don't sleep")
# After: smaller, role-specific interfaces.
class Workable(ABC):
@abstractmethod
def work(self): ...
class Eatable(ABC):
@abstractmethod
def eat(self): ...
class Sleepable(ABC):
@abstractmethod
def sleep(self): ...
class HumanWorker(Workable, Eatable, Sleepable):
def work(self):
print("coding")
def eat(self):
print("eating lunch")
def sleep(self):
print("sleeping")
class Robot(Workable):
def work(self):
print("welding")
Analysis
The Rectangle/Square example shows how inheritance can be misused to model an 'is-a' relationship from geometry class that does not hold for mutable behavior: a Square is a Rectangle mathematically, but Square cannot honor Rectangle's independent-setter contract. The fix was not to patch Square, but to stop assuming Rectangle's mutable API is the right shared abstraction at all -- an immutable Shape interface with only area() sidesteps the broken contract entirely. Similarly, RobotWorker was forced to implement eat() and sleep() only to throw exceptions, which is a strong signal that FatWorker should be segregated. After the split, Robot implements only Workable, and no client that only needs 'can this work?' is coupled to eating or sleeping behavior it will never use.
Cricket analogy: The Rectangle/Square trap mirrors assuming a specialist finisher can bat anywhere in the order like a versatile top-order batsman; the fix isn't patching the finisher's role but recognizing 'batsman' needs a narrower shared interface, just as forcing an umpire to also do groundskeeping (like FatWorker's eat/sleep) shows the role needs splitting so umpires only implement officiating duties.
Key Takeaways
- LSP: a subclass must be usable anywhere its base class is expected, without breaking caller assumptions.
- Overriding a method to throw an exception or silently change unrelated state is a common LSP red flag.
- The Rectangle/Square example shows that 'is-a' in the real world does not always translate to safe inheritance in code.
- ISP: split large, multi-purpose interfaces into small, role-specific ones.
- A class implementing methods it must stub out with NotImplementedError signals an ISP violation.
Practice what you learned
1. In the classic Rectangle/Square example, why does making Square a subclass of Rectangle violate LSP?
2. Which of these is the strongest signal of an ISP violation?
3. What does the Liskov Substitution Principle require about subclass method preconditions and postconditions?
4. In the ISP example, what problem does splitting FatWorker into Workable, Eatable, and Sleepable solve?
Was this page helpful?
You May Also Like
Single Responsibility and Open/Closed Principles
Learn to split classes by responsibility and extend behavior with polymorphism instead of editing existing code.
Dependency Inversion
Distinguish the Dependency Inversion Principle from Dependency Injection and apply constructor injection against an abstraction.
Polymorphism and Abstraction
Distinguishing polymorphism (same interface, different implementations) from abstraction (hiding implementation details) using Python's abc module.
Inheritance
How single inheritance, `super().__init__()`, and method overriding let subclasses reuse and specialize parent class behavior.
Related Reading
Related Study Notes in Software Engineering
Browse all study notesMicroservices Study Notes
Software Architecture · 30 topics
Software EngineeringTesting & TDD Study Notes
Software Testing · 30 topics
Software EngineeringDesign Patterns Study Notes
Software Design · 30 topics
Software EngineeringGit & Version Control Study Notes
Bash · 40 topics
Software EngineeringSystem Design Study Notes
Architecture · 40 topics