Liskov Substitution Principle
Subtypes must be fully substitutable for their base types.
Liskov Substitution Principle (LSP): A Complete Guide with Examples in 11 Programming Languages
The Liskov Substitution 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 any of the desirable properties of the program. In plain terms: a subclass must be fully substitutable for its parent class. Any code that works with the base type must work equally correctly with any derived type — without knowing which one it has.
In this comprehensive guide, we'll explore LSP with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin — complete with language-specific best practices, common violations, and clear guidance on when and how to apply this fundamental principle.
Table of Contents
- What is the Liskov Substitution Principle?
- Why Follow LSP?
- LSP Comparison: Violation vs. Compliance
- LSP Explained
- Class Diagram
- Beginner-Friendly Example
- Production-Ready Example
- Real-World Use Cases
- Language-Specific Mistakes and Anti-Patterns
- Frequently Asked Questions
- Key Takeaways
What is the Liskov Substitution Principle?
LSP is the third of the five SOLID principles, introduced by Barbara Liskov in 1987 and formalized as: if φ(x) is a property provable about objects x of type T, then φ(y) should be true for objects y of type S where S is a subtype of T.
The classic illustration is the Rectangle/Square problem. Mathematically, a square is a rectangle. So it seems natural to make Square extend Rectangle. But a rectangle has independently settable width and height. When you set a square's width, its height must also change (to remain square). This breaks code that expects a rectangle to hold its height when its width changes: a function that sets width to 5 and height to 3 and expects an area of 15 will get 9 when given a square. The square is not substitutable for the rectangle — LSP is violated.
The violation surfaces not as a compilation error but as a runtime surprise: behavior that was correct for the parent is wrong for the child. LSP violations are particularly insidious because they are often hidden — the code compiles, type checks pass, but correctness silently breaks when a subtype is substituted.
LSP is fundamentally a contract: the subtype must honor every behavioral guarantee that the supertype's callers rely on.
Why Follow LSP?
LSP provides several critical benefits:
- Predictability: Code that depends on an abstraction behaves correctly regardless of which concrete implementation it receives — no surprises.
- Safe Polymorphism: Collections of base types can be iterated and used without
instanceofchecks or type-specific handling — every element behaves as expected. - Reliable Inheritance: Subclasses extend behavior without breaking the contracts the parent type established.
- Honest Abstraction: An inheritance or interface relationship that satisfies LSP is one that genuinely models an "is-a" relationship — not a "kind-of" or "similar-to."
- Reduced Defensive Code: When LSP holds, callers don't need to check types or guard against unexpected behaviors — the contract is guaranteed.
- Test Reusability: Tests written against the base type pass for all subtypes — a natural test for LSP compliance.
LSP Comparison: Violation vs. Compliance
| Dimension | LSP Violation | LSP Compliant |
|---|---|---|
| Substitution | Subtype breaks code that worked with base type | Any subtype can replace the base type transparently |
| Method behavior | Subtype throws unexpected exceptions or returns wrong values | Subtype honors all postconditions of the base type |
| Preconditions | Subtype demands stricter inputs than the base type | Subtype accepts the same or weaker inputs |
| Postconditions | Subtype provides weaker guarantees than the base type | Subtype provides the same or stronger guarantees |
| instanceof checks | Required to handle type-specific behavior | Never needed — polymorphism handles it |
| Test reliability | Tests written for base type fail for some subtypes | Tests for base type pass for all subtypes |
LSP vs. Related Concepts:
- LSP vs. Inheritance: Inheritance is the mechanism; LSP is the contract that makes inheritance safe. Inheritance without LSP is inheritance that surprises — it looks like "is-a" but behaves like "kind-of."
- LSP vs. OCP: OCP says existing code should not need modification for new subtypes. LSP is what makes OCP possible — OCP relies on the promise that new subtypes are correctly substitutable.
- LSP vs. Interface Implementation: LSP applies equally to interface implementation. An interface defines a contract; every implementor must honor it fully.
LSP Explained
The Behavioral Contract
LSP is about behavioral compatibility, not just structural compatibility. A subtype can compile fine and still violate LSP if it:
- Strengthens preconditions: The base method accepts any positive number; the subtype rejects numbers below 10. Code passing 5 worked with the base type but fails with the subtype.
- Weakens postconditions: The base method guarantees a non-null return value; the subtype may return null. Code that dereferences the return value worked with the base type but crashes with the subtype.
- Throws unexpected exceptions: The base method never throws; the subtype throws
NotSupportedException. Code that calls the method without a try/catch worked with the base type but crashes with the subtype. - Changes invariants: The base type guarantees
width * height == area; the subtype breaks this invariant. Callers relying on this invariant get wrong answers.
The Square/Rectangle Problem
This is the canonical LSP violation:
Rectangle has: setWidth(w), setHeight(h), area() = width * height
Square extends Rectangle — but must keep width == height at all times
client code:
shape.setWidth(5)
shape.setHeight(3)
assert shape.area() == 15 // Passes for Rectangle, FAILS for Square (gets 9)
Square is not substitutable for Rectangle despite the mathematical "is-a" relationship. The correct design: both Square and Rectangle implement a Shape interface. They don't inherit from each other.
How to Detect LSP Violations
Three practical tests:
- The Substitution Test: Replace every instance of the base type in your code with the subtype. Does all existing behavior still hold? If not, the subtype violates LSP.
- The
instanceofTest: If your code containsif (obj instanceof SubType)to handle a subtype specially, the subtype is not truly substitutable — an LSP violation. - The Base Type Test Suite Test: Run the test suite you wrote for the base type against each subtype. All tests should pass. Any failure signals an LSP violation.
Class Diagram
Here's the UML class diagram showing a shape hierarchy that correctly follows LSP:
Beginner-Friendly Example
Let's illustrate LSP with the classic Rectangle/Square problem — first showing the violation, then the correct design.
from __future__ import annotations
import math
from abc import ABC, abstractmethod
# ── ❌ LSP VIOLATION: Square extends Rectangle ─────────────────────
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
def __repr__(self) -> str:
return f"Rectangle({self._width}×{self._height})"
class Square(Rectangle):
"""Violates LSP: overrides set_width/set_height to keep sides equal,
breaking the Rectangle contract that width and height are independent."""
def set_width(self, width: float) -> None:
self._width = width
self._height = width # Side effect: changes height too!
def set_height(self, height: float) -> None:
self._width = height # Side effect: changes width too!
self._height = height
def test_area(shape: Rectangle) -> None:
"""Code written for Rectangle — should work for any 'Rectangle'."""
shape.set_width(5)
shape.set_height(3)
expected = 15
actual = shape.area()
status = "✅" if actual == expected else "❌ LSP VIOLATED"
print(f" {shape.__class__.__name__}: area={actual} (expected={expected}) {status}")
print("=== LSP Violation ===\n")
test_area(Rectangle(2, 2)) # ✅ 15
test_area(Square(2)) # ❌ 9 — LSP violated!
# ── ✅ LSP COMPLIANT: Both implement the same interface ────────────
class Shape(ABC):
"""Base contract — all shapes fulfill these guarantees."""
@abstractmethod
def area(self) -> float:
"""Returns the area. Always ≥ 0."""
@abstractmethod
def perimeter(self) -> float:
"""Returns the perimeter. Always ≥ 0."""
@abstractmethod
def describe(self) -> str:
"""Human-readable description of the shape."""
class LSPRectangle(Shape):
def __init__(self, width: float, height: float) -> None:
assert width > 0 and height > 0, "Dimensions must be positive"
self._width = width
self._height = height
def area(self) -> float: return self._width * self._height
def perimeter(self) -> float: return 2 * (self._width + self._height)
def describe(self) -> str: return f"Rectangle {self._width}×{self._height}"
class LSPSquare(Shape):
def __init__(self, side: float) -> None:
assert side > 0, "Side must be positive"
self._side = side
def area(self) -> float: return self._side ** 2
def perimeter(self) -> float: return 4 * self._side
def describe(self) -> str: return f"Square {self._side}×{self._side}"
class LSPCircle(Shape):
def __init__(self, radius: float) -> None:
assert radius > 0, "Radius must be positive"
self._radius = radius
def area(self) -> float: return math.pi * self._radius ** 2
def perimeter(self) -> float: return 2 * math.pi * self._radius
def describe(self) -> str: return f"Circle r={self._radius}"
def print_shape_info(shape: Shape) -> None:
"""Works correctly for ANY Shape — LSP guarantees this."""
print(f" {shape.describe()}: area={shape.area():.2f}, perimeter={shape.perimeter():.2f}")
print("\n=== LSP Compliant ===\n")
shapes: list[Shape] = [
LSPRectangle(5, 3),
LSPSquare(4),
LSPCircle(3),
]
for shape in shapes:
print_shape_info(shape) # Same code, correct results for all subtypes
Production-Ready Example
Now let's build a document storage and processing system — a production scenario where LSP violations appear subtly. We'll define a DocumentStore abstraction and show how different storage backends (in-memory, file system, cloud) must honor the same behavioral contract.
🐍 Python
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
import os
# ── Domain Model ───────────────────────────────────────────────────
@dataclass
class Document:
id: str
title: str
content: str
created_at: datetime = field(default_factory=datetime.now)
version: int = 1
# ── Behavioral Contract ────────────────────────────────────────────
class DocumentStore(ABC):
"""All implementations MUST honor these behavioral guarantees:
- save(doc) always succeeds or raises DocumentStoreError
- find(id) returns the exact document last saved with that id, or None
- delete(id) ensures find(id) returns None afterwards
- list_all() returns every document currently in the store
"""
@abstractmethod
def save(self, document: Document) -> None:
"""Save or update a document. Must not fail silently."""
@abstractmethod
def find(self, doc_id: str) -> Optional[Document]:
"""Return the document with the given id, or None if not found."""
@abstractmethod
def delete(self, doc_id: str) -> bool:
"""Delete document. Return True if deleted, False if not found."""
@abstractmethod
def list_all(self) -> list[Document]:
"""Return all stored documents. Never return None; return [] if empty."""
@abstractmethod
def count(self) -> int:
"""Return the number of stored documents."""
class DocumentStoreError(Exception):
pass
# ── Compliant Implementation 1: In-Memory Store ────────────────────
class InMemoryDocumentStore(DocumentStore):
"""Backed by a dictionary. Suitable for tests and lightweight usage."""
def __init__(self) -> None:
self._store: dict[str, Document] = {}
def save(self, document: Document) -> None:
self._store[document.id] = document
print(f" [Memory] Saved: {document.id} '{document.title}'")
def find(self, doc_id: str) -> Optional[Document]:
return self._store.get(doc_id)
def delete(self, doc_id: str) -> bool:
if doc_id in self._store:
del self._store[doc_id]
return True
return False
def list_all(self) -> list[Document]:
return list(self._store.values())
def count(self) -> int:
return len(self._store)
# ── Compliant Implementation 2: File System Store ──────────────────
class FileSystemDocumentStore(DocumentStore):
"""Persists documents as text files. Suitable for local persistence."""
def __init__(self, base_dir: str) -> None:
self._base_dir = base_dir
os.makedirs(base_dir, exist_ok=True)
def _path(self, doc_id: str) -> str:
return os.path.join(self._base_dir, f"{doc_id}.txt")
def save(self, document: Document) -> None:
try:
with open(self._path(document.id), 'w') as f:
f.write(f"{document.title}\n{document.content}")
print(f" [FS] Saved: {document.id} → {self._path(document.id)}")
except OSError as e:
raise DocumentStoreError(f"Failed to save {document.id}: {e}") from e
def find(self, doc_id: str) -> Optional[Document]:
path = self._path(doc_id)
if not os.path.exists(path):
return None
try:
with open(path) as f:
lines = f.readlines()
return Document(id=doc_id, title=lines[0].strip(), content="".join(lines[1:]).strip())
except OSError as e:
raise DocumentStoreError(f"Failed to read {doc_id}: {e}") from e
def delete(self, doc_id: str) -> bool:
path = self._path(doc_id)
if os.path.exists(path):
os.remove(path)
return True
return False
def list_all(self) -> list[Document]:
docs = []
for filename in os.listdir(self._base_dir):
if filename.endswith(".txt"):
doc_id = filename[:-4]
doc = self.find(doc_id)
if doc:
docs.append(doc)
return docs
def count(self) -> int:
return len([f for f in os.listdir(self._base_dir) if f.endswith(".txt")])
# ── Consumer — closed, depends only on DocumentStore contract ──────
class DocumentService:
"""Uses any DocumentStore without knowing its implementation.
Correct for InMemory, FileSystem, and any future store."""
def __init__(self, store: DocumentStore) -> None:
self._store = store
def create_document(self, title: str, content: str) -> Document:
import uuid
doc = Document(id=str(uuid.uuid4())[:8], title=title, content=content)
self._store.save(doc)
return doc
def get_document(self, doc_id: str) -> Optional[Document]:
return self._store.find(doc_id)
def remove_document(self, doc_id: str) -> bool:
return self._store.delete(doc_id)
def list_documents(self) -> list[Document]:
docs = self._store.list_all()
print(f" Found {len(docs)} document(s):")
for doc in docs:
print(f" - [{doc.id}] '{doc.title}'")
return docs
def document_count(self) -> int:
return self._store.count()
# ── LSP Test: Same tests must pass for ALL implementations ─────────
def run_store_tests(store: DocumentStore, label: str) -> None:
print(f"\n=== Testing {label} ===\n")
service = DocumentService(store)
# Test 1: Create and find
doc = service.create_document("Meeting Notes", "Agenda: budget review")
found = service.get_document(doc.id)
assert found is not None, "LSP: find() must return saved document"
assert found.title == "Meeting Notes", "LSP: find() must return exact document"
print(f" ✅ Create + Find: '{found.title}'")
# Test 2: List
service.create_document("Project Plan", "Phase 1: research")
docs = service.list_documents()
assert len(docs) >= 2, "LSP: list_all() must include all saved documents"
print(f" ✅ List: {len(docs)} documents")
# Test 3: Count
count = service.document_count()
assert count >= 2, "LSP: count() must match actual stored documents"
print(f" ✅ Count: {count}")
# Test 4: Delete
deleted = service.remove_document(doc.id)
assert deleted, "LSP: delete() must return True for existing document"
gone = service.get_document(doc.id)
assert gone is None, "LSP: find() must return None after delete()"
print(f" ✅ Delete: document gone after removal")
# Test 5: Delete non-existent
deleted_again = service.remove_document("nonexistent-id")
assert not deleted_again, "LSP: delete() must return False for missing document"
print(f" ✅ Delete non-existent: returned False correctly")
# ── Client ─────────────────────────────────────────────────────────
if __name__ == "__main__":
import tempfile
import shutil
# Same tests, different stores — all pass because LSP is honored
run_store_tests(InMemoryDocumentStore(), "In-Memory Store")
tmp_dir = tempfile.mkdtemp()
try:
run_store_tests(FileSystemDocumentStore(tmp_dir), "File System Store")
finally:
shutil.rmtree(tmp_dir)
print("\n✅ All stores passed the LSP behavioral contract tests.")
Real-World Use Cases
LSP violations appear in surprising places in production systems. Understanding where they commonly arise helps you recognize and prevent them:
Read-Only Repository Subclass: A UserRepository base class defines find(), save(), update(), and delete(). A ReadOnlyUserRepository subclass throws NotImplementedException on save(), update(), and delete(). Code that calls these methods on the base type breaks when given the read-only subtype — an LSP violation. Correct design: define ReadableRepository and WritableRepository as separate interfaces.
Throwing Base Class Methods: A Logger interface has log(level, message). An AsyncLogger subclass throws NotYetFlushed exceptions if flush() wasn't called first. Code using Logger has no idea it needs to call flush() — the contract is broken. Correct design: the async logger's log() method always succeeds (buffering internally), and flush() is an additional method.
Mutable vs. Immutable Collections: A List<T> base type allows add() and remove(). An ImmutableList<T> subclass throws UnsupportedOperationException on mutations. Code that takes a List<T> and calls add() breaks — LSP is violated. Java's Collections.unmodifiableList() is a famous example of this. Correct design: ImmutableList implements ReadableList, not List.
Narrowed Return Types That Break Callers: A parse(input: string): any method returns flexible data. A subclass overrides it to return only string, throwing if the result would be a number. Callers expecting numeric results from the base type get exceptions from the subtype. LSP requires that postconditions be honored or strengthened, never weakened.
Language-Specific Mistakes and Anti-Patterns
❌ Mistake #1: Raising NotImplementedError in Subclass Methods
# BAD: ReadOnlyCache violates the Cache contract by throwing on write methods
class Cache:
def get(self, key: str) -> str | None: ...
def set(self, key: str, value: str) -> None: ...
def delete(self, key: str) -> bool: ...
class ReadOnlyCache(Cache):
def get(self, key: str) -> str | None:
return self._backend.get(key)
def set(self, key: str, value: str) -> None:
raise NotImplementedError("Read-only cache!") # LSP VIOLATION
def delete(self, key: str) -> bool:
raise NotImplementedError("Read-only cache!") # LSP VIOLATION
# GOOD: Separate interfaces for reading and writing
class ReadableCache(ABC):
@abstractmethod
def get(self, key: str) -> str | None: ...
class WritableCache(ReadableCache, ABC):
@abstractmethod
def set(self, key: str, value: str) -> None: ...
@abstractmethod
def delete(self, key: str) -> bool: ...
class ReadOnlyCache(ReadableCache):
def get(self, key: str) -> str | None:
return self._backend.get(key)
# No set() or delete() — the interface doesn't demand them
❌ Mistake #2: Overriding Methods to Tighten Preconditions
# BAD: Subclass accepts narrower input than base class
class FileProcessor:
def process(self, filepath: str) -> str:
"""Processes any file path."""
with open(filepath) as f:
return f.read()
class JsonFileProcessor(FileProcessor):
def process(self, filepath: str) -> str:
if not filepath.endswith(".json"):
raise ValueError("Only .json files accepted!") # Tightened precondition — LSP VIOLATION
with open(filepath) as f:
return f.read()
# GOOD: The subclass accepts the same input range; its specialization is in how it processes
class JsonFileProcessor(FileProcessor):
def process(self, filepath: str) -> str:
content = super().process(filepath)
# Specialization: parse and re-serialize for normalization
import json
return json.dumps(json.loads(content), indent=2)
Frequently Asked Questions
Q1: Is the Square/Rectangle problem only about mathematics?
No. It's about behavioral contracts. Mathematically, a square is a rectangle — but in an OOP context, a Square class is not a behavioral substitute for a Rectangle class when the Rectangle class allows independent width and height mutation. The problem isn't the geometry; it's the contract. If Rectangle were immutable — with no setters — then Square could safely inherit from it because the independence invariant doesn't exist. The lesson: inheritance must respect behavioral contracts, not just mathematical relationships.
Q2: How do I know if an inheritance relationship violates LSP?
Run the Substitution Test: imagine replacing every reference to the base type in your codebase with the subtype. Does every piece of code still behave correctly? If you must add an instanceof check somewhere, or if a test that passed for the base type fails for the subtype, LSP is violated. The base-type test suite test is particularly reliable: write tests that capture the behavioral contract of the base type, then run them against every subtype. Failures signal violations.
Q3: Can LSP be violated through interface implementation (not just inheritance)?
Yes. LSP applies equally to interface implementation. An interface defines a behavioral contract: if Serializer.serialize() guarantees non-null output and NullSerializer returns null, the contract is violated even though NullSerializer is "implementing an interface," not "inheriting a class." The mechanism is different, but the violation is the same: callers relying on the guarantee are broken.
Q4: Is it ever acceptable to throw NotSupportedException in a subtype method?
Rarely, and only when the base type's contract explicitly allows it. If the base type's documentation says "some implementations may throw NotSupportedException for optional operations," then a throwing subtype is within the contract. Java's Collection interface does this deliberately for optional mutation methods. But when the base type makes no such allowance, throwing in a subtype silently breaks callers — an LSP violation. If you're considering throwing, it usually signals that the abstraction is too broad and should be split into separate interfaces.
Q5: How does LSP relate to contravariance and covariance?
LSP has formal type-theoretic rules: method parameters may be contravariant (accept broader types), and return types may be covariant (return narrower types). A subtype that accepts only a narrower input type (strengthened precondition) or returns a broader output type (weakened postcondition) violates LSP. In practice: a subtype's methods should accept the same or wider range of inputs than the parent, and return the same or narrower type than the parent. Most languages enforce this statically for typed return types but not for preconditions, which is why behavioral LSP violations can still occur at runtime.
Q6: Should I never use deep inheritance hierarchies?
Deep hierarchies increase LSP risk — each level of inheritance adds another set of contracts that must be honored all the way down. Shallow hierarchies (at most 2-3 levels) are easier to reason about. When hierarchies grow deep, consider using composition instead: an object that holds a base object and delegates to it can extend or restrict behavior at the composition level, where the contracts are more explicit. Go and Rust's absence of inheritance entirely and reliance on interfaces and composition is in part a response to this risk.
Q7: How does Go avoid LSP violations?
Go has no inheritance — only interfaces (implemented implicitly) and struct composition. This makes the classic inheritance-based LSP violations structurally impossible. In Go, a Rectangle and a Square would both implement a Shape interface independently, with no subtype relationship between them. The Square/Rectangle problem simply cannot arise. However, LSP can still be violated at the behavioral level: a Go struct implementing an interface can still return wrong values or panic unexpectedly. The principle remains relevant even without inheritance.
Key Takeaways
🎯 Core Concept: The Liskov Substitution Principle states that objects of a supertype should be replaceable with objects of any subtype without breaking the program's correctness. LSP is a behavioral contract: a subtype must honor every guarantee that callers rely on when using the base type. Violations occur not at compile time but at runtime — through unexpected exceptions, wrong return values, broken invariants, or silent no-ops.
🔑 Key Benefits:
- Safe polymorphism: Code that works with a base type works correctly with any subtype — no
instanceofchecks, no runtime surprises - Predictable inheritance: Subclasses extend behavior without breaking the contracts the parent established
- Reusable tests: Tests for the base type pass for all subtypes — violations are caught immediately
- Honest "is-a" relationships: If LSP holds, the inheritance or interface relationship genuinely models substitutability, not just code reuse
🌐 Language-Specific Highlights:
- Python: Use the
abstractmethoddecorator to enforce the full contract; avoidNotImplementedErrorin subclasses — it signals that the abstraction is too broad - TypeScript: Use
readonlyproperties on immutable implementations; never widen a return type (fromUsertoUser | null) in a subclass without updating the interface - Java: The
Collections.unmodifiableList()pattern is a canonical LSP violation — prefer separateReadableListandMutableListinterfaces - JavaScript: Be explicit about return type consistency — a subclass that turns a synchronous return into a
Promisesilently breaks all callers - C#: Avoid
virtualmethods with empty base implementations — they invite silent no-op overrides; use interfaces to make capabilities explicit - PHP: Use
readonlyfor immutable data; never override a parent method to throw an exception type not in the parent's contract - Go: Structural (implicit) interfaces naturally enforce LSP — two structs implementing the same interface are independent, no subtype risks
- Rust: Trait objects (
Box<dyn Trait>) enforce the behavioral contract at the type system level; LSP violations reduce to returning wrongResultvariants - Dart: Use
abstract interface classto define strict behavioral contracts;sealedhierarchies help enforce exhaustive handling - Swift: Protocols are pure behavioral contracts; avoid
protocolrequirements that only some conformers implement meaningfully - Kotlin:
sealed interfacegives exhaustive handling;data classprovides value equality consistency; avoid deep open class hierarchies
📋 When LSP Is Violated:
- A subtype throws an exception that the base type's contract does not allow
- A subtype requires stricter inputs (narrowed precondition) than the base type
- A subtype returns weaker guarantees (weakened postcondition) than the base type
- Code contains
instanceofchecks to handle a subtype specially - Tests written for the base type fail when run against the subtype
⚠️ How to Fix LSP Violations:
- Split a too-broad interface into smaller, capability-specific interfaces (ReadableList / MutableList)
- Use composition instead of inheritance when the "is-a" relationship holds mathematically but not behaviorally
- Make the base type's contract explicit in documentation or by using checked exceptions
- Prefer flat, interface-based designs over deep inheritance hierarchies
🛠️ Best Practices Across Languages:
- Write the contract before writing the subtype: Document the behavioral guarantees of the base type — preconditions, postconditions, invariants — before any subtype is written
- Run base-type tests against all subtypes: Test-based LSP checking is the most reliable way to detect violations early
- Prefer composition over deep inheritance: A composed object can override behavior at the composition level without subtype contract risks
- Use interface segregation: Small, focused interfaces reduce the risk of "empty implementation" LSP violations — ISP and LSP are natural allies
- Avoid
NotImplementedand silent no-ops: If a subtype can't implement a method meaningfully, the abstraction is too broad — split it
The Liskov Substitution Principle is the principle that makes polymorphism trustworthy. When LSP holds, you can pass any subtype where the base type is expected and the system behaves correctly — no defensive checks, no runtime surprises, and no code paths that work for some types but not others.
Want to dive deeper into SOLID principles? Check out our comprehensive guides on:
- Single Responsibility Principle (SRP)
- Open/Closed Principle (OCP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP) Each guide includes examples in all 11 programming languages with language-specific best practices.