SOLID Principles · The Five Principles

Liskov Substitution Principle

Subtypes must honor the contract of their supertype, so code written for the parent keeps working with any child.

  • 4 min read
  • 9 practice questions
  • 2 graded problems
On this page
  1. Description
  2. The principle
  3. How to apply it, step by step
  4. Where you'll see it
  5. Which tool for which job?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

Description

The Liskov Substitution Principle says that an object of a subtype must work anywhere its supertype is expected, without surprising the code that uses it. It's what makes polymorphism trustworthy: if every implementation keeps the parent's promises, callers never need to check which one they got. This skill covers what those promises are, the classic ways subclasses break them, and how to model types so they don't.

Here's the classic example of breaking it:

Java

class Rectangle {
    protected int width, height;
    void setWidth(int w)  { width = w; }
    void setHeight(int h) { height = h; }
    int area() { return width * height; }
}

class Square extends Rectangle {          // "a square IS a rectangle", right?
    void setWidth(int w)  { width = w; height = w; }
    void setHeight(int h) { width = h; height = h; }
}

void stretch(Rectangle r) {
    r.setWidth(5);
    r.setHeight(4);
    assert r.area() == 20;                // fails for a Square: area is 16
}

Python

class Rectangle:
    def __init__(self):
        self.width = self.height = 0

    def set_width(self, w: int) -> None:
        self.width = w

    def set_height(self, h: int) -> None:
        self.height = h

    def area(self) -> int:
        return self.width * self.height


class Square(Rectangle):                  # "a square IS a rectangle", right?
    def set_width(self, w: int) -> None:
        self.width = self.height = w

    def set_height(self, h: int) -> None:
        self.width = self.height = h


def stretch(r: Rectangle) -> None:
    r.set_width(5)
    r.set_height(4)
    assert r.area() == 20                 # fails for a Square: area is 16

Mathematically a square is a rectangle. In code, Square breaks a promise every Rectangle user relies on: setting the width doesn't change the height.

We'll go from simple to more technical:

  1. The principle: behavior, not just method signatures.
  2. The contract rules: preconditions, postconditions, invariants, exceptions.
  3. How to check a hierarchy and fix it, step by step.
  4. Where substitution fails in real libraries.

The principle

Objects of a subtype must be usable anywhere the supertype is expected, without breaking the program.

Subtyping is about behavior, not just method signatures. A subclass must keep the parent's contract:

  • It may not demand more from callers (no stronger preconditions: rejecting inputs the parent accepted).
  • It may not promise less (no weaker postconditions: returning results the parent ruled out).
  • It must preserve the parent's invariants (width and height are independent).
  • It shouldn't throw new kinds of exceptions callers can't expect.

How to apply it, step by step

  1. Write down what callers of the parent can rely on (the contract) - ideally as tests.
  2. For each subclass, check it keeps every one of those promises.
  3. If a subclass can't (a Penguin that can't fly(), a Square that can't resize freely), the hierarchy is wrong.
  4. Fix the model: split capabilities into separate interfaces, use composition, or make the types immutable (an immutable Square never has to fake being resized).
  5. Run the same contract tests against every implementation.

Where you'll see it

  • List implementations: ArrayList and LinkedList differ in speed but behave identically - you can swap them freely. (Python: list, tuple and range are all collections.abc.Sequences.)
  • Collections.unmodifiableList throws on add() - a well-known, documented compromise in the JDK's collection design. Python's collections.abc avoids it by splitting the roles: tuple is a Sequence but not a MutableSequence, so it simply has no append to refuse.
  • Any plugin or strategy interface: every implementation must behave the way the calling code expects.

Which tool for which job?

The situationModel it as
Subtypes really do keep every promise of the parentinheritance or an interface - substitution is safe
Only some types can do something (fly, resize, write)a separate capability interface (Flyer, MutableList)
A "special case" that can't keep the parent's invariantseparate types, or immutable types whose operations return new objects
Reusing code without promising substitutabilitycomposition
Checking that every implementation behaves the sameone contract test suite run against each implementation

Common mistakes

MistakeWhat happensDo this instead
A subclass throws "not supported" for an inherited methodcallers of the parent crashsplit the capability into its own interface
A subclass rejects inputs the parent acceptedcallers' valid calls start failingaccept at least what the parent accepts
A subclass returns results the parent ruled out (null, negative)callers' assumptions breakpromise at least what the parent promises
instanceof / isinstance checks in callers to special-case a subtypesubstitution has already failedfix the hierarchy, not the callers
An override that skips a step callers rely on (events, validation)silent behavior changeskeep the documented side effects

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. A ReadOnlyList extends ArrayList throws on add(). Why is that an LSP problem?

Show answer

Code written for ArrayList (or List) expects add to work; the subclass refuses behavior its parent promised, so it can't be substituted safely.

2. How does making Rectangle and Square immutable avoid the classic violation?

Show answer

Without setters, there's no "resize width only" promise to break. Resizing returns a new shape - resizing a square to 5x4 simply returns a rectangle.

3. Can a subclass accept a wider range of inputs than its parent?

Show answer

Yes - weakening preconditions is allowed. What's forbidden is demanding more (rejecting inputs the parent accepted).

Key takeaways

  • A subtype must keep every promise its supertype makes - behavior, not just signatures.
  • No stronger preconditions, no weaker postconditions, preserved invariants.
  • UnsupportedOperationException overrides and instanceof checks in callers are red flags.
  • Fix with separate capabilities, composition or immutability - and contract tests.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Pick the tool

The situationModel it as
Subtypes keep every promiseinheritance / interface
Only some types have a capabilitya separate capability interface
A special case can't keep an invariantseparate types, or immutable types
Reuse without substitutabilitycomposition
Proving all implementations agreeone contract test suite per interface

Snippets worth knowing by heart

Java

interface Shape { int area(); }
record Rectangle(int width, int height) implements Shape {
    public int area() { return width * height; }
    Rectangle withWidth(int w) { return new Rectangle(w, height); }
}
record Square(int side) implements Shape {
    public int area() { return side * side; }
}

Python

@dataclass(frozen=True)
class Rectangle:
    width: int
    height: int

    def area(self) -> int:
        return self.width * self.height

    def with_width(self, w: int) -> "Rectangle":
        return Rectangle(w, self.height)

@dataclass(frozen=True)
class Square:
    side: int

    def area(self) -> int:
        return self.side * self.side

The traps

  • "Not supported" overrides - the parent promised it.
  • Stronger preconditions - rejecting what the parent accepted.
  • Weaker postconditions - returning what the parent ruled out.
  • Type checks in callers - substitution already failed.

Staff-level points, one line each

  • Liskov and Wing (1994): weaker preconditions, stronger postconditions, invariants, and the history rule.
  • Type checkers check signatures (variance); behavior is up to contracts and tests.
  • Mutable containers are invariant; read-only ones (Sequence, ? extends) covariant; PECS for wildcards.
  • Java arrays are covariant: items[0] = 42 on a String[] throws ArrayStoreException.
  • Java forbids new checked exceptions in overrides; everything else is by contract.
  • JDK compromises: optional operations (unmodifiable lists throw on add), Timestamp/Date asymmetric equals.
  • A contract suite caught a cache whose get raised KeyError for missing keys.
  • API versions follow the same rules - a new required field is a breaking change; consumer-driven contracts check it.

Say it in an interview

  • "A square that can't be resized like a rectangle isn't a rectangle in code - I'd model them separately."
  • "Read-only callers take Sequence[Animal], so they accept a list of dogs safely."
  • "Every cache implementation runs the same contract tests."
  • "Making that request field required would break every existing client."

Practice

This skill comes with 9 multiple-choice questions that tell you which misconception a wrong answer reveals and 2 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Liskov Substitution Principle free

Squares, Rectangles and Honest Resizing

MediumProCoding problemJava · Python

Every Shape promises (see the comment on the interface): withWidth(w) changes only the width, withHeight(h) changes only the height, and shapes are immutable - resizing returns a new shape.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Senders That Keep Their Promise

MediumProCoding problemJava · Python

Notifications go out through interchangeable MessageSenders. The base class documents the contract every sender must keep: it accepts any non-empty message, of any length, and delivers the whole message - joining the returned parts gives back exactly the original text.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Liskov Substitution Principle free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime