100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Python

Liskov Substitution and Interface Segregation

Understand why subtypes must honor their base type's contract and why fat interfaces should be split into focused ones.

SOLID PrinciplesIntermediate11 min readJul 8, 2026
Analogies

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

python
# --- 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

Was this page helpful?

Topics covered

#Python#SoftwareEngineeringStudyNotes#SoftwareEngineering#LiskovSubstitutionAndInterfaceSegregation#Liskov#Substitution#Interface#Segregation#StudyNotes#SkillVeris