Tier 1 · Foundations

Core Concepts

OOP Pillars

SOLID Principles

Creational Patterns

Structural Patterns

Behavioral Patterns

Tier 2

Tier 3

Tier 4

Tier 5

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; }
}

The same caller exposes a broken contract

Run the same rectangle test with Rectangle and SquareBad; the square override changes both dimensions and breaks the expected area.

Rectangle lets the caller set width and height independently.

Text version
  1. Rectangle lets the caller set width and height independently.
  2. The rectangle test passes because five times three is fifteen.
  3. With SquareBad, setting width to five also sets height to five.
  4. Inside SquareBad, setting height to three first changes width to three.
  5. Height also becomes three, so the area becomes nine.
  6. The caller still expects fifteen, so substitution breaks the rectangle contract.
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 a MutableList base whose contract promises add() works, but its add() throws; now code that calls add() on any MutableList 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

Choose a repair that fits the contract

A shared Shape base keeps Square out of the Rectangle hierarchy, composition replaces inheritance used only for reuse, and Design by Contract writes preconditions and postconditions down.

Square and Rectangle both extend Shape, so Square never inherits the Rectangle setters.

Text version
  1. Square and Rectangle both extend Shape, so Square never inherits the Rectangle setters.
  2. Composition delegates area to a formula instead of inheriting, at the cost of more classes.
  3. Design by Contract documents behavioral promises that static type checks do not verify.
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. The List contract marks add() as an optional operation, so this is technically allowed — but 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 — iterating a plain QuerySet yields model instances (values() returns one that yields dicts). 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

Reading

On this page

My notes

Sign in to keep highlights and notes.

Glossary history

Click dotted jargon to save explanations here.