Liskov Substitution Principle (LSP)

8 min read

Reading Progress0%
Object-Oriented Design Index
Tier 1 -- Foundations
Tier 2
Tier 3
Tier 4
Tier 5
Object-Oriented Design Index
Tier 1 -- Foundations
Tier 2
Tier 3
Tier 4
Tier 5

Liskov Substitution Principle (LSP)

1. What Is It?

The Principle states that if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the correctness of the program. Robert C. Martin's formulation: "functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it."

Named for Barbara Liskov, who introduced the concept of behavioral subtyping in her 1987 keynote "Data and Hierarchy," LSP is fundamentally about behavioral contracts, not just type compatibility. A subclass that compiles and runs but surprises callers with unexpected behavior — throwing exceptions the base class never throws, weakening post-conditions, or returning wrong ranges — violates LSP even if it passes type checks.

Without LSP, becomes a trap. Code that works with a Bird breaks when handed an Ostrich (which can't fly). Callers must add isinstance checks to handle "special" subclasses — a clear sign the hierarchy is broken.


QUICK CHECK

A PaymentProcessor base class has a charge(amount) method that always returns a transaction ID on success and never throws exceptions — errors are communicated via a returned error code instead. A CryptoPaymentProcessor subclass extends it but throws a NetworkTimeoutException when the blockchain is unreachable. Which statement best describes this design?

Choose one answer

2. How It Works

LSP defines a contract between base class and subclass:

  1. Preconditions cannot be strengthened — a subclass cannot demand more from callers than the base class did
  2. Postconditions cannot be weakened — a subclass cannot promise less than the base class did
  3. Invariants must be preserved — properties the base class guarantees must hold in all subclasses
  4. No new exceptions — a subclass cannot throw exception types the base class never throws

The canonical LSP violation is the Rectangle/Square problem. A Square is a Rectangle geometrically — but substituting Square for Rectangle in code that sets width and height independently breaks invariants.

Mermaid Class Diagram

Python Example

from abc import ABC, abstractmethod


# LSP VIOLATION — Square extends Rectangle and breaks invariants

class Rectangle:
    def __init__(self, width: float, height: float) -> None:
        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 SquareBad(Rectangle):
    """Violates LSP: set_width changes height too, breaking Rectangle's contract."""

    def set_width(self, width: float) -> None:
        self._width = width
        self._height = width  # Silently changes height!

    def set_height(self, height: float) -> None:
        self._width = height  # Silently changes width!
        self._height = height


def assert_rectangle_invariant(rect: Rectangle) -> None:
    """This test passes for Rectangle but FAILS for SquareBad."""
    rect.set_width(5)
    rect.set_height(3)
    assert rect.area() == 15, f"Expected 15, got {rect.area()}"  # Fails for SquareBad!


# LSP FIX — Flatten the hierarchy under a shared abstraction

class Shape(ABC):
    @abstractmethod
    def area(self) -> float: ...

    @abstractmethod
    def perimeter(self) -> float: ...


class RectangleFixed(Shape):
    def __init__(self, width: float, height: float) -> None:
        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

    def perimeter(self) -> float:
        return 2 * (self._width + self._height)


class SquareFixed(Shape):
    """Square does NOT extend Rectangle. No contract is broken."""

    def __init__(self, side: float) -> None:
        self._side = side

    def set_side(self, side: float) -> None:
        self._side = side

    def area(self) -> float:
        return self._side ** 2

    def perimeter(self) -> float:
        return 4 * self._side

Java Example

// LSP VIOLATION
public class Rectangle {
    protected double width;
    protected double height;

    public void setWidth(double width) { this.width = width; }
    public void setHeight(double height) { this.height = height; }
    public double area() { return width * height; }
}

public class SquareBad extends Rectangle {
    @Override
    public void setWidth(double width) {
        this.width = width;
        this.height = width; // Silently breaks Rectangle's contract!
    }

    @Override
    public void setHeight(double height) {
        this.width = height;
        this.height = height;
    }
}

// This test passes for Rectangle but fails for SquareBad
public static void assertRectangleInvariant(Rectangle rect) {
    rect.setWidth(5);
    rect.setHeight(3);
    assert rect.area() == 15 : "Expected 15, got " + rect.area();
}


// LSP FIX — shared abstraction, separate hierarchies

public interface Shape {
    double area();
    double perimeter();
}

public class RectangleFixed implements Shape {
    private double width;
    private double height;

    public RectangleFixed(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public void setWidth(double width) { this.width = width; }
    public void setHeight(double height) { this.height = height; }

    @Override
    public double area() { return width * height; }

    @Override
    public double perimeter() { return 2 * (width + height); }
}

public class SquareFixed implements Shape {
    private double side;

    public SquareFixed(double side) { this.side = side; }

    public void setSide(double side) { this.side = side; }

    @Override
    public double area() { return side * side; }

    @Override
    public double perimeter() { return 4 * side; }
}

QUICK CHECK

A Rectangle class has independent setWidth() and setHeight() methods. A Square subclass overrides both so that calling setWidth(5) also sets the height to 5. A method that accepts a Rectangle, calls setWidth(5) then setHeight(3), and expects area() to return 15 will fail when passed a Square. Which LSP rule does this violation break?

Choose one answer

3. Variants & Comparisons

LSP failures come in several flavors:

Violation TypeWhat HappensExample
Strengthened preconditionSubclass requires more from caller than baseBird.fly() works always; Ostrich.fly() throws NotImplementedError
Weakened postconditionSubclass promises less than baseSquare.setWidth() no longer guarantees only width changes
Broken invariantSubclass breaks a property the base class guaranteedReadOnlyList extends List but add() throws; now code that calls add() on any List can fail
Narrowed exception contractSubclass throws new checked exceptions the caller doesn't expectSubclass payment method throws NetworkException; base class method never throws
ApproachHow It WorksProsConsBest For
Flatten to shared basePut Square and Rectangle under ShapeClean, no broken contractsLoses "is-a" relationship if it was genuinely usefulWhen the mathematical "is-a" doesn't translate to behavioral "is-a"
Composition over inheritanceSquare has a side; delegate area to a formulaNo inheritance; no LSP riskMore classesWhen subclassing is only used for code reuse, not polymorphism
Design by ContractDocument preconditions/postconditions explicitly (Eiffel, Python's assert)Self-documentingNot enforced at compile time in most languagesLibrary code where contracts must be explicit

QUICK CHECK

A base class PaymentProcessor has a charge(amount) method that never throws any exceptions — it returns a result object indicating success or failure. A subclass StripeProcessor overrides charge(amount) but throws a NetworkException when the Stripe API is unreachable. Which LSP violation does this represent, and what is its practical consequence?

Choose one answer

4. When to Use It (and When NOT To)

Apply LSP checks when:

  • You're inheriting from a class you didn't write (third-party base class)
  • Subclass methods throw exceptions the base class doesn't
  • You have isinstance checks in code that handles a base class — a red flag that substitution is failing
  • A subclass method does nothing (empty override) — it's likely violating postconditions

Anti-patterns:

  • NotImplementedError in subclass: Inheriting from Bird to get common bird attributes, but Ostrich.fly() raises NotImplementedError. The hierarchy is wrong — FlyingBird and NonFlyingBird should be separate types.
  • Extending for code reuse only: Using to reuse helper methods rather than to establish a polymorphic contract. Use instead.
  • Override that widens/narrows return type improperly: Returning None from a method that the base class guarantees returns a value.

Decision triggers:

  • "If you see if isinstance(obj, SpecificSubclass): do_special_thing — LSP is probably violated."
  • "If a subclass method body is pass or raise NotImplementedError — question whether it belongs in the hierarchy."

QUICK CHECK

A backend developer inherits from a base PaymentProcessor class that guarantees its process() method always returns a transaction ID (a non-null string). The RefundProcessor subclass overrides process() and returns None when no refund is applicable. Code throughout the system that calls process() on any PaymentProcessor now crashes with unexpected NoneType errors. What is the core LSP violation here, and what should the developer do instead?

Choose one answer

5. Real-World Usage

Java's java.util.List and Collections.unmodifiableList(): unmodifiableList() returns a List where mutating methods throw UnsupportedOperationException. This is technically an LSP violation — callers who assume List.add() works will fail. Java chose pragmatism over purity here, but it's a well-known footgun.

Python's ABCs (collections.abc): Python's Sequence, Mapping, and MutableSequence are designed with LSP in mind. Sequence guarantees __getitem__ and __len__; MutableSequence extends that with mutation methods. Subclasses that implement the ABC must honor the full contract — the abstract methods enforce what callers depend on.

Django's QuerySet: Django's QuerySet is lazy — it promises that iteration yields model instances. Subclasses that specialize QuerySet must honor this contract; if a subclass __iter__ returned something other than model instances, all downstream code would break silently.


QUICK CHECK

Java's Collections.unmodifiableList() returns a List where calling add() throws UnsupportedOperationException. Why is this considered a Liskov Substitution Principle (LSP) violation?

Choose one answer

6. Interview Cheat Sheet

Key sentences to say:

  1. "LSP is about behavioral contracts, not just type compatibility. A subclass can pass type checks and still violate LSP if it surprises callers."
  2. "The Square/Rectangle problem is the canonical example — geometrically correct, but behaviorally broken because independent width/height setting is part of Rectangle's contract."
  3. "The fix is usually to flatten the hierarchy: put Square and Rectangle under a shared Shape instead of making one extend the other."
  4. "A isinstance check in code that handles a base type is almost always a sign that LSP is violated — the caller shouldn't need to know the concrete subtype."

Common follow-up questions:

"How is LSP different from just 'don't override methods badly'?"

LSP is precise: it defines which overrides are allowed. You can override a method — but you cannot strengthen preconditions, weaken postconditions, or throw new exception types. Those rules make substitution safe.

"Doesn't Java's Comparable violate LSP?"

Not if implemented correctly. The contract for compareTo is well-defined and consistent. Violations happen when developers implement compareTo inconsistently with equals, which breaks the TreeSet/TreeMap guarantees.

"What about Python's duck typing — does LSP apply?"

Yes, but implicitly. In Python, LSP violations show up as AttributeError or TypeError at runtime when a duck-typed object doesn't honor the expected protocol. ABCs make these contracts explicit.

Connections to other concepts:

  • LSP is a prerequisite for Open/Closed Principle — if substitution breaks, you can't safely extend; you must modify
  • LSP violations are often fixed by applying over — replacing SquareBad(Rectangle) with Square(Shape) with an internal side formula
  • Principle helps prevent LSP violations — narrow interfaces reduce the contract a subclass must honor
Glossary History

Click dotted jargon to save explanations here.

Glossary History

Click dotted jargon to save explanations here.