Liskov Substitution Principle
Subtypes honour the contract.
A subtype must work wherever its supertype is promised. No surprise throws. No weakened guarantees. The contract you inherited is the contract you keep.
Opinion
I have watched far too many engineers extend a class because the “is-a” sentence sounded right in English, then hand-wave the contract away. A Square is-a Rectangle, until the function that worked for Rectangles silently breaks when you set the width and the height changes.2“The Liskov Substitution Principle,” C++ Report, 1996. The modern English statement and the Square-Rectangle pedagogy. Restated as the L of SOLID in Agile Software Development (2002). The grammar lies. Behavioural subtyping is the only kind of subtyping that matters; everything else is autocomplete.
The rule is simpler than the Latin makes it sound. Read every throws in the override; a subtype that throws something the supertype did not has narrowed the postcondition the caller depends on, and Liskov is dead. Read every parameter check; a subtype that rejects an input the supertype accepted has strengthened a precondition, and Liskov is dead. Read every public-state mutation; a subtype that lets the object reach a state the supertype said was impossible has weakened the invariant, and Liskov is dead. Three reads, three smells, one rule.
In practice the language is mostly TypeScript or Java rather than C++ now, and the violation is mostly an interface implementation rather than a class hierarchy. Pair Liskov with A11 Dependency Inversion: the abstraction the high-level depends on must be honoured by every concrete adapter that satisfies it. Pair it with F6 Encapsulation, because a contract is only enforceable when the public surface is the only surface. The deep cost of breaking Liskov is paid in tests: a mock that satisfies the type system but not the behaviour ships green and breaks production. The fix is the rule, not the test.
Copy a note and link
Grab this short comment and drop it into a PR comment or an LLM chat to prompt the right change.
Subtypes honour the contract. Preconditions no stronger, postconditions no weaker, invariants preserved. Read every `extends` with the substitution question: can I replace parent with child at every call site without breaking the caller? If no, the inheritance is wrong. /tenet/liskov-substitution-principle/A9
AI eyes only
Rule: subtypes honour the contract of the supertype. Substitution must not change behaviour.
Reject: a subtype that throws on a method the supertype declares total. Reject: a subtype that strengthens preconditions or weakens postconditions. Reject: extending purely for code reuse.
Generate: when emitting extends or implements, list the preconditions, postconditions and invariants of the supertype. Confirm the subtype honours each before shipping.
Diagnostic: every test against the supertype must pass against the subtype. If a single test fails, the relationship is composition or interface, not subtyping.
Why?
- Liskov is about behaviour, not signatures. The compiler checks signature subtyping; the rule checks the contract. The gap is where production bugs live.
- No subtype throws an exception the supertype didn't. The most common Liskov violation in practice; the easiest to spot in review.
- Subtypes do not narrow accepted inputs. A method that accepted any positive integer in the parent cannot accept only even integers in the child without breaking every caller.
- Subtypes do not weaken returned guarantees. A method that returned a non-empty list in the parent cannot return an optional in the child without breaking every caller.
- Subtypes do not allow the object to reach states the parent declared impossible. The Square-Rectangle problem is exactly this: setting width changes height, breaking the Rectangle invariant.
- Mocks honour the rule too. A test double that satisfies the type but not the behaviour ships green and breaks production. Liskov tells you the mock is the violation.
- Pairs with A10 Interface Segregation (small interfaces are easier to honour) and A11 Dependency Inversion (the abstraction at the seam is the contract). The three SOLID-stack rules compose.
- Agents extend by surface fit, not by contract. With the rule loaded, every new
extendsneeds a sentence about preconditions, postconditions and invariants — the discipline a senior reviewer brings.
Origins
Barbara Liskov gave the OOPSLA 1987 keynote “Data Abstraction and Hierarchy” and published it in SIGPLAN Notices 23(5) the following year.1“Data Abstraction and Hierarchy,” OOPSLA 1987 keynote, SIGPLAN Notices 23(5), 1988. The substitution requirement appears as a single sentence; the field built half a paradigm on top of it. The substitution requirement appears as a single sentence: “What is wanted here is something like the following substitution property: if for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T.” Liskov, who built the CLU language with Stephen Zilles in the 1970s and went on to win the Turing Award in 2008, was articulating what type theorists had been circling for a decade.
The formalisation came in Liskov & Jeannette Wing's 1994 ACM TOPLAS 16(6) paper “A Behavioral Notion of Subtyping.”3“A Behavioral Notion of Subtyping,” ACM TOPLAS 16(6), 1994. The formal paper introducing history rules and the precondition / postcondition / invariant triple. Required reading. The paper introduces history rules, the precondition / postcondition / invariant triple, and proofs that signature-only subtyping (the kind a compiler can check) is not sufficient: a type system can let a class extend another without honouring the parent's contract, and Java's and C++'s type systems do exactly that. Behavioural subtyping is what callers depend on; signature subtyping is what the compiler enforces; the gap is where Liskov violations live.
The principle reached working developers via Robert C. Martin's SOLID work. Martin's 1996 C++ Report piece on the LSP gave the modern English statement — “Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it” — and the Square-Rectangle example that every introductory OO course now uses.2“The Liskov Substitution Principle,” C++ Report, 1996. The modern English statement and the Square-Rectangle pedagogy. Restated as the L of SOLID in Agile Software Development (2002). Agile Software Development (2002) and Clean Architecture (2017) restated it as the “L” in SOLID; the same rule, three layers of compression, indistinguishable in substance.
Bertrand Meyer's Object-Oriented Software Construction (1988, 2nd ed. 1997) was the parallel European tradition. Meyer built design-by-contract into Eiffel as a first-class language feature: every routine carries explicit require and ensure clauses; subtyping is checked against them at compile time.4Object-Oriented Software Construction, 2nd ed. (Prentice Hall, 1997). Eiffel's design-by-contract — pre-/post-conditions and invariants as first-class language constructs. The European tradition that meets Liskov & Wing in the middle. Meyer's contract clauses are the syntactic surface Liskov & Wing's paper formalises semantically; the two traditions met when Java added pre-/post-condition libraries, when C# added Code Contracts, and when modern TypeScript variance rules began to check exactly the substitution Liskov described.
Quotes
What is wanted here is something like the following substitution property: if for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T.
Subtypes must satisfy whatever is provable about supertype objects. Methods of subtypes require no more and ensure no less than the corresponding methods of supertypes.
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
Favour object composition over class inheritance.
Evidence
Twenty external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.
- 01The original. The substitution requirement appears as a single sentence; the field built half a paradigm on top of it.
- 02The formal paper. Introduces history rules and the precondition / postcondition / invariant triple. The peer-reviewed source the rule rests on.
- 03Modern English statement and the Square-Rectangle pedagogy that every introductory OO course now uses.
- 04Clean Architecture, Ch. 9SupportsThe architectural-scale reading. The supertype is the published interface contract; substitutability is the rule for adapters.
- 05Eiffel's design-by-contract — pre-/post-conditions and invariants as first-class language constructs. The European tradition that meets Liskov & Wing in the middle.
Sixteen sources spanning the original (Liskov 1987 keynote; Liskov & Wing 1994), the SOLID canon (Martin), and the Eiffel design-by-contract tradition (Meyer). Modern language-level treatments (TypeScript variance, Scala, Rust trait coherence) extend the row. The qualifiers further down carry the “most inheritance is wrong” reading; the opposers carry the formal-methods steelman.
Examples
// Before: Hedgehog overrides walk() to throw. Mammal[] crashes.class Mammal { walk(): void { console.log("step"); }}class Hedgehog extends Mammal { walk(): void { throw new Error("hedgehogs roll, they don't walk"); }}function marchMammals(mammals: Mammal[]): void { for (const m of mammals) m.walk();}
// After: move() is the contract. Each subtype honours it.interface Mammal { move(): void; }class Fox implements Mammal { move(): void { console.log("trot"); }}class Hedgehog implements Mammal { move(): void { console.log("roll"); }}function marchMammals(mammals: Mammal[]): void { for (const m of mammals) m.move();}
Enforcement
Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| @typescript-eslint/method-signature-style | typescript-eslint | method-shorthand declarations that opt into bivariance — the loophole that lets a subtype accept a wider parameter than the parent. |
| @typescript-eslint/no-misused-promises | typescript-eslint | Promise-returning functions used where a sync function was expected. The most common LSP violation in modern TypeScript. |
| @typescript-eslint/explicit-function-return-type | typescript-eslint | missing return-type annotations. Without an explicit return type, an override can quietly widen the postcondition. |
| @typescript-eslint/no-unsafe-return | typescript-eslint | returns of `any` from a typed function. The other shape of postcondition weakening — claim a type, return whatever. |
| @typescript-eslint/no-unsafe-argument | typescript-eslint | passing `any` where a typed parameter is required. The precondition end of the same problem. |
| @typescript-eslint/prefer-readonly | typescript-eslint | mutable fields that should be readonly. Mutable shared state is the easiest path to invariant breakage. |
eslint.config.mjsconfiguration snippet
import tseslint from 'typescript-eslint';
export default tseslint.config({
files: ['**/*.{ts,tsx}'],
rules: {
'@typescript-eslint/method-signature-style': ['error', 'property'],
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/no-base-to-string': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
'@typescript-eslint/no-unsafe-argument': 'error',
'@typescript-eslint/explicit-function-return-type': ['error', {
allowExpressions: false,
allowTypedFunctionExpressions: true,
}],
'@typescript-eslint/prefer-readonly': 'error',
}
});AI rules
.cursor/rules/a9-liskov-substitution.mdc---
description: Prickles A9 — Liskov Substitution Principle
globs: "**/*.{ts,tsx,js,jsx,py,java,php}"
alwaysApply: false
---
## Prickles A9 — Liskov Substitution Principle
A subtype must work wherever its supertype is promised. No surprise throws. No weakened guarantees.
Three reads on every override. Preconditions: the subtype accepts at least every input the parent accepts. Postconditions: the subtype guarantees at least every output the parent guarantees. Invariants: the subtype keeps the parent's promised states honest.
Read every `extends` and `implements` in the diff with the substitution question. If you cannot replace the parent with the child at every call site without breaking the caller, the inheritance is wrong.
Refuse a subtype that throws what the parent didn't, or that returns the parent's promised type as optional, or that mutates state the parent declared invariant. The compiler may accept it; the contract does not.Repo layout, CI, and ESLint wiring for these paths live on /implementation — not repeated on every tenet.
Counter-argument
The strongest steelman is “prefer composition over inheritance.” The argument: inheritance is the wrong primitive in the first place; without provable substitutability, extends should never have been used. The Gang of Four said it in 1994;9Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994). “Favour object composition over class inheritance.” The composition-first reading that pairs with Liskov: prefer composition; when you must inherit, honour the contract. Sandi Metz spent two books restating it; modern languages from Go to Rust dropped inheritance entirely and lost nothing. The implication for “Liskov Substitution” is sharp. If the rule mostly names when not to inherit, then teaching the rule trains people to inherit too often. Better to drop the primitive than to keep policing its use.
Counter-argument retort
The composition-over-inheritance counter is correct on its own terms and Liskov absorbs it. The rule is not a recommendation to use inheritance; it is the rule for when you do. Modern practice is exactly the right reading: prefer composition; reach for extends only when the relationship genuinely is is-a; when you do, honour the contract. The Gang of Four's “favour composition over inheritance” and Liskov's “subtypes must honour the contract” are not opposing principles; they are the same principle applied at two adjacent decision points.9Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994). “Favour object composition over class inheritance.” The composition-first reading that pairs with Liskov: prefer composition; when you must inherit, honour the contract.
The signature-only-is-enough objection — “the compiler enforces variance; what more do you need?” — is the harder one. Liskov & Wing's 1994 paper answers it directly: signature subtyping is a necessary but not sufficient condition.3“A Behavioral Notion of Subtyping,” ACM TOPLAS 16(6), 1994. The formal paper introducing history rules and the precondition / postcondition / invariant triple. Required reading. A subclass that throws a NotImplementedException from a method the parent guaranteed to return is signature-correct and behaviour-broken. A subclass that narrows the accepted argument type is signature-correct and Liskov-violating. The compiler checks the type; the rule checks the contract; the two are different things and only one of them stops the production bug.
The strongest critique stays the formal-methods one. Hillel Wayne and others have pointed out that “the contract” is rarely written down; the rule is enforceable in Eiffel and checked by hand everywhere else.5“Why Don't People Use Formal Methods?” (2018). The honest critique: contracts are rarely written down outside Eiffel; the rule is enforceable by hand and rarely enforced. The reply is partial: in TypeScript, the contract is most of the type — if you have used branded types and discriminated unions well (per T1 Domain-Driven Types), the type system is closer to the contract than the OO tradition assumes. Where the contract really cannot live in the type, a property-based test of the supertype's laws (Liskov-Wing-shaped, applied to every implementation) is the missing artefact. Both moves narrow the “contract on paper” gap that the formal-methods critique correctly identifies.
The genuine residue is that Liskov is one rule in a SOLID stack that earns its place because the failure mode it prevents (silent runtime breakage) is uniquely expensive. Pair it with A10 Interface Segregation (small interfaces are easier to honour), A11 Dependency Inversion (the abstraction at the seam is the contract), and F6 Encapsulation (the public surface is the only surface), and the four-rule combination covers the failure modes individually. Liskov is the one that catches the bugs that ship green.
Notes
- [1]Barbara Liskov — “Data Abstraction and Hierarchy,” OOPSLA 1987 keynote, SIGPLAN Notices 23(5), 1988. The substitution requirement appears as a single sentence; the field built half a paradigm on top of it.
- [2]Robert C. Martin — “The Liskov Substitution Principle,” C++ Report, 1996. The modern English statement and the Square-Rectangle pedagogy. Restated as the L of SOLID in Agile Software Development (2002).
- [3]Barbara Liskov & Jeannette Wing — “A Behavioral Notion of Subtyping,” ACM TOPLAS 16(6), 1994. The formal paper introducing history rules and the precondition / postcondition / invariant triple. Required reading.