On this page
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:
- The principle: behavior, not just method signatures.
- The contract rules: preconditions, postconditions, invariants, exceptions.
- How to check a hierarchy and fix it, step by step.
- 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
- Write down what callers of the parent can rely on (the contract) - ideally as tests.
- For each subclass, check it keeps every one of those promises.
- If a subclass can't (a
Penguinthat can'tfly(), aSquarethat can't resize freely), the hierarchy is wrong. - Fix the model: split capabilities into separate interfaces, use composition, or make the types immutable (an immutable
Squarenever has to fake being resized). - Run the same contract tests against every implementation.
Where you'll see it
Listimplementations:ArrayListandLinkedListdiffer in speed but behave identically - you can swap them freely. (Python:list,tupleandrangeare allcollections.abc.Sequences.)Collections.unmodifiableListthrows onadd()- a well-known, documented compromise in the JDK's collection design. Python'scollections.abcavoids it by splitting the roles:tupleis aSequencebut not aMutableSequence, so it simply has noappendto refuse.- Any plugin or strategy interface: every implementation must behave the way the calling code expects.
Which tool for which job?
| The situation | Model it as |
|---|---|
| Subtypes really do keep every promise of the parent | inheritance 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 invariant | separate types, or immutable types whose operations return new objects |
| Reusing code without promising substitutability | composition |
| Checking that every implementation behaves the same | one contract test suite run against each implementation |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
| A subclass throws "not supported" for an inherited method | callers of the parent crash | split the capability into its own interface |
| A subclass rejects inputs the parent accepted | callers' valid calls start failing | accept at least what the parent accepts |
A subclass returns results the parent ruled out (null, negative) | callers' assumptions break | promise at least what the parent promises |
instanceof / isinstance checks in callers to special-case a subtype | substitution has already failed | fix the hierarchy, not the callers |
| An override that skips a step callers rely on (events, validation) | silent behavior changes | keep 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.
UnsupportedOperationExceptionoverrides andinstanceofchecks in callers are red flags.- Fix with separate capabilities, composition or immutability - and contract tests.