bigonotes
CS ConceptsSOLID Principles

LSP vs ISP

How the Liskov Substitution and Interface Segregation principles differ in goal, symptom and fix.

2 min readLast updated SOLID Principles

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 Penguin is a Bird).
  • 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 Fish can 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.

Found a mistake? Tell me on LinkedIn ↗

Further reading#