8 min read
Object-Oriented Design Index
Tier 1 -- Foundations
Core Concepts
SOLID Principles
Creational Patterns
Structural Patterns
Tier 2
Tier 3
Tier 4
Tier 5
Object-Oriented Design Index
Tier 1 -- Foundations
Core Concepts
SOLID Principles
Creational Patterns
Structural Patterns
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.
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?
2. How It Works
LSP defines a contract between base class and subclass:
- Preconditions cannot be strengthened — a subclass cannot demand more from callers than the base class did
- Postconditions cannot be weakened — a subclass cannot promise less than the base class did
- Invariants must be preserved — properties the base class guarantees must hold in all subclasses
- 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; } }
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?
3. Variants & Comparisons
LSP failures come in several flavors:
| Violation Type | What Happens | Example |
|---|---|---|
| Strengthened precondition | Subclass requires more from caller than base | Bird.fly() works always; Ostrich.fly() throws NotImplementedError |
| Weakened postcondition | Subclass promises less than base | Square.setWidth() no longer guarantees only width changes |
| Broken invariant | Subclass breaks a property the base class guaranteed | ReadOnlyList extends List but add() throws; now code that calls add() on any List can fail |
| Narrowed exception contract | Subclass throws new checked exceptions the caller doesn't expect | Subclass payment method throws NetworkException; base class method never throws |
| Approach | How It Works | Pros | Cons | Best For |
|---|---|---|---|---|
| Flatten to shared base | Put Square and Rectangle under Shape | Clean, no broken contracts | Loses "is-a" relationship if it was genuinely useful | When the mathematical "is-a" doesn't translate to behavioral "is-a" |
| Composition over inheritance | Square has a side; delegate area to a formula | No inheritance; no LSP risk | More classes | When subclassing is only used for code reuse, not polymorphism |
| Design by Contract | Document preconditions/postconditions explicitly (Eiffel, Python's assert) | Self-documenting | Not enforced at compile time in most languages | Library code where contracts must be explicit |
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?
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
isinstancechecks 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:
NotImplementedErrorin subclass: Inheriting fromBirdto get common bird attributes, butOstrich.fly()raisesNotImplementedError. The hierarchy is wrong —FlyingBirdandNonFlyingBirdshould 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
Nonefrom 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
passorraise NotImplementedError— question whether it belongs in the hierarchy."
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?
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.
Java's Collections.unmodifiableList() returns a List where calling add() throws UnsupportedOperationException. Why is this considered a Liskov Substitution Principle (LSP) violation?
6. Interview Cheat Sheet
Key sentences to say:
- "LSP is about behavioral contracts, not just type compatibility. A subclass can pass type checks and still violate LSP if it surprises callers."
- "The Square/Rectangle problem is the canonical example — geometrically correct, but behaviorally broken because independent width/height setting is part of Rectangle's contract."
- "The fix is usually to flatten the hierarchy: put Square and Rectangle under a shared
Shapeinstead of making one extend the other." - "A
isinstancecheck 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
compareTois well-defined and consistent. Violations happen when developers implementcompareToinconsistently withequals, which breaks theTreeSet/TreeMapguarantees.
"What about Python's duck typing — does LSP apply?"
Yes, but implicitly. In Python, LSP violations show up as
AttributeErrororTypeErrorat 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)withSquare(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.