LSP and ISP are easy to confuse because the classic examples (birds that can't fly, fish that can't run) look alike. They solve different problems: LSP is about inheritance keeping its promises, ISP is about interfaces not asking for too much.
LSP: don't break the parent's promises#
LSP is about subclasses honoring their parent's contract.
- Goal: children behave consistently with their parent.
- About: is-a relationships (a
Penguinis aBird). - Problem: "the subclass lies about what it can do".
class Bird:
def fly(self) -> None:
print("Flying")
class Penguin(Bird):
def fly(self) -> None:
# Breaks the parent's promise
raise NotImplementedError("Can't fly!")
# Code expecting a Bird gets a Penguin and crashes.
ISP: don't force unused methods#
ISP is about interfaces staying small enough to implement honestly.
- Goal: classes only implement what they use.
- About: can-do relationships (a
Fishcan swim). - Problem: "I must implement ten methods but only need two".
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def fly(self) -> None: ...
@abstractmethod
def swim(self) -> None: ...
@abstractmethod
def run(self) -> None: ...
class Fish(Animal):
def fly(self) -> None:
pass # can't: forced to fake it
def swim(self) -> None:
print("Swimming") # real
def run(self) -> None:
pass # can't: forced to fake it
Different problems, different fixes#
LSP is fixed by reshaping the hierarchy; ISP by splitting the interface:
from abc import ABC, abstractmethod
# LSP fix: restructure the hierarchy so no class promises
# flight it can't deliver
class Bird:
pass
class FlyingBird(Bird):
def fly(self) -> None:
print("Flying")
class Penguin(Bird):
def swim(self) -> None:
print("Swimming")
# ISP fix: split the fat interface
class Flyable(ABC):
@abstractmethod
def fly(self) -> None: ...
class Swimmable(ABC):
@abstractmethod
def swim(self) -> None: ...
class Fish(Swimmable):
def swim(self) -> None:
print("Swimming")
FlyingBird().fly()
Fish().swim()
| LSP | ISP | |
|---|---|---|
| Concerned with | Inheritance (is-a) | Interfaces (can-do) |
| Question | Can the child properly replace the parent? | Should this interface be split? |
| Symptom | Child breaks or surprises callers of the parent | Class implements methods it can't support |
| Fix | Better hierarchy | Smaller, focused interfaces |
| Analogy | A substitute teacher who can actually teach | A job posting that lists 50 skills when 3 are needed |
The two often show up together: a fat interface (ISP problem) forces implementers to throw for some methods, which then breaks substitutability (LSP problem). Splitting the interface usually fixes both.
Key takeaways#
- LSP: "don't lie about what you inherited".
- ISP: "don't make me implement what I don't use".
- LSP is fixed by reshaping the class hierarchy; ISP by splitting interfaces.
- Raising
NotImplementedError(or doing nothing) in an override is a smell for both.