Use when creating subclasses or implementing interfaces. Use when tempted to override methods with exceptions or no-ops. Use when inheritance hierarchy feels wrong.
Liskov Substitution Principle (LSP)
Overview
Subtypes must be substitutable for their base types without altering program correctness.
If S is a subtype of T, objects of type T can be replaced with objects of type S without breaking the program. Subclasses must honor the contracts of their parent classes.
When to Use
- Creating a class that extends another class
- Overriding methods from a parent class
- Implementing an interface
- Feeling like you need to throw exceptions in overridden methods
- Inheritance hierarchy feels "forced"
The Iron Rule
NEVER create a subclass that breaks the expectations of the parent class.
No exceptions:
- Not for "it's the standard approach"
- Not for "I'll note it as an anti-pattern"
- Not for "the requirements say to extend"
- Not throwing exceptions in overridden methods
- Not making overridden methods no-ops
Providing violating code "with a caveat" is still providing violating code.
Detection: The Substitution Test
Ask: "Can I replace every instance of Parent with Child without breaking anything?"
function processRectangle(rect: Rectangle): void {
rect.setWidth(5);
rect.setHeight(10);
assert(rect.getArea() === 50); // Always true for Rectangle
}
// If Square extends Rectangle:
const square = new Square(5);
processRectangle(square); // FAILS! Area is 100, not 50
If substitution breaks code, you have an LSP violation.
Detection: Override Smells
These overrides indicate LSP violations:
// ❌ VIOLATION: Throwing in override
class Penguin extends Bird {
fly(): void {
throw new Error("Penguins can't fly"); // Breaks callers expecting fly()
}
}
// ❌ VIOLATION: No-op override
class ReadOnlyStorage extends FileStorage {
write(path: string, content: string): void {
// Silently does nothing - breaks caller expectations
}
}
// ❌ VIOLATION: Changing behavior semantics
class Square extends Rectangle {
setWidth(w: number): void {
this.width = w;
this.height = w; // Changes height too - breaks expectations
}
}
The Correct Pattern: Composition & Interfaces
Don't force inheritance. Use interfaces to define capabilities.
Square/Rectangle Problem
// ✅ CORRECT: Separate types, shared interface
interface Shape {
getArea(): number;
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
getArea(): number { return this.width * this.height; }
setWidth(w: number): void { this.width = w; }
setHeight(h: number): void { this.height = h; }
}
class Square implements Shape {
constructor(private size: number) {}
getArea(): number { return this.size * this.size; }
setSize(s: number): void { this.size = s; }
}
Bird/Penguin Problem
// ✅ CORRECT: Capability interfaces
interface Flyable {
fly(): void;
}
abstract class Bird {
abstract eat(): void;
}
class Sparrow extends Bird implements Flyable {
eat(): void { /* ... */ }
fly(): void { /* ... */ }
}
class Penguin extends Bird {
eat(): void { /* ... */ }
swim(): void { /* ... */ }
// No fly() - doesn't promise what it can't deliver
}
ReadOnly Problem
// ✅ CORRECT: Separate interfaces
interface Readable {
read(path: string): string;
}
interface Writable {
write(path: string, content: string): void;
delete(path: string): void;
}
class FileStorage implements Readable, Writable {
read(path: string): string { /* ... */ }
write(path: string, content: string): void { /* ... */ }
delete(path: string): void { /* ... */ }
}
class AuditLogStorage implements Readable {
read(path: string): string { /* ... */ }
// No write/delete - doesn't extend something it can't honor
}
Pressure Resistance Protocol
1. "Just Override and Throw"
Pressure: "Handle the fact they can't fly by throwing an error"
Response: Throwing in an override violates the contract. Code expecting fly() will crash.
Action: Restructure with interfaces. Don't inherit methods you can't honor.
2. "It's the Standard Approach"
Pressure: "Override-and-throw is the standard way to do this"
Response: "Standard" doesn't mean correct. This pattern causes runtime failures.
Action: Use composition and interfaces instead.
3. "The Requirements Say Extend"
Pressure: "Square must extend Rectangle per the requirements"
Response: Requirements that mandate LSP violations are wrong. Push back.
Action:
"A Square extending Rectangle violates LSP and will cause bugs.
I recommend: [correct approach with interfaces].
Should I implement the correct structure, or document this as known tech debt?"
4. "I'll Note It's an Anti-Pattern"
Pressure: Internal rationalization
Response: Providing bad code with a caveat is still providing bad code.
Action: Provide only the correct solution. Don't implement the violation.
Red Flags - STOP and Reconsider
If you notice ANY of these, you're about to violate LSP:
- Overriding a method to throw an exception
- Overriding a method to do nothing (no-op)
- Overriding a method to change its fundamental behavior
- Subclass can't do everything the parent can
- Inheritance feels forced or unnatural
- Using
instanceofchecks to handle subtypes differently
All of these mean: Use composition and interfaces instead.
Quick Reference
| Violation | Correct Approach |
|---|---|
| Square extends Rectangle | Both implement Shape interface |
| Penguin extends Bird (with fly) | Bird base + Flyable interface |
| ReadOnlyStorage extends Storage | Separate Readable/Writable interfaces |
| Child throws in override | Child shouldn't extend that parent |
| Child no-ops an override | Child shouldn't extend that parent |
Common Rationalizations (All Invalid)
| Excuse | Reality |
|---|---|
| "It's the standard approach" | Common doesn't mean correct. |
| "I provided a caveat" | Bad code with warnings is still bad code. |
| "Requirements say extend" | Requirements can be wrong. Push back. |
| "Throwing makes it explicit" | Throwing breaks callers. Compile errors are better. |
| "No-op is safe" | Silent failures hide bugs. |
| "It's just for this one case" | One violation leads to more. Fix it properly. |
The Bottom Line
If a subclass can't fully substitute for its parent, don't use inheritance.
Use interfaces to define capabilities. Use composition to share behavior. Never override methods with exceptions or no-ops.
When asked to create violating inheritance: restructure with interfaces instead. Don't provide the violation "with a caveat."
You Might Also Like
Related Skills

coding-agent
Run Codex CLI, Claude Code, OpenCode, or Pi Coding Agent via background process for programmatic control.
openclaw
add-uint-support
Add unsigned integer (uint) type support to PyTorch operators by updating AT_DISPATCH macros. Use when adding support for uint16, uint32, uint64 types to operators, kernels, or when user mentions enabling unsigned types, barebones unsigned types, or uint support.
pytorch
at-dispatch-v2
Convert PyTorch AT_DISPATCH macros to AT_DISPATCH_V2 format in ATen C++ code. Use when porting AT_DISPATCH_ALL_TYPES_AND*, AT_DISPATCH_FLOATING_TYPES*, or other dispatch macros to the new v2 API. For ATen kernel files, CUDA kernels, and native operator implementations.
pytorch
skill-writer
Guide users through creating Agent Skills for Claude Code. Use when the user wants to create, write, author, or design a new Skill, or needs help with SKILL.md files, frontmatter, or skill structure.
pytorch
implementing-jsc-classes-cpp
Implements JavaScript classes in C++ using JavaScriptCore. Use when creating new JS classes with C++ bindings, prototypes, or constructors.
oven-sh
implementing-jsc-classes-zig
Creates JavaScript classes using Bun's Zig bindings generator (.classes.ts). Use when implementing new JS APIs in Zig with JSC integration.
oven-sh