Liskov Substitution Principle
Objects of a subclass must be usable anywhere the superclass is expected, without changing the correctness of the program.
Subtypes must behave.
The Liskov Substitution Principle (LSP) says if S is a subtype of
T, you should be able to use an S wherever a T is expected and
everything still works correctly. A subclass must honor the promises of its parent.
If code written for Bird assumes it can fly(), then a
Penguin subclass that can't fly breaks that code — a sign the inheritance is wrong.
# Problem: Square "is-a" Rectangle but breaks Rectangle's contract. class Rectangle: def __init__(self, w, h): self.w, self.h = w, h def set_width(self, w): self.w = w def set_height(self, h): self.h = h def area(self): return self.w * self.h class Square(Rectangle): def set_width(self, w): self.w = self.h = w # also changes height! def set_height(self, h): self.w = self.h = h def stretch(rect: Rectangle): rect.set_width(5) rect.set_height(4) assert rect.area() == 20 # fails for Square — LSP violation
# Fix: don't force an is-a that lies. Separate types, shared protocol. from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self): ... class Rectangle(Shape): def __init__(self, w, h): self.w, self.h = w, h def area(self): return self.w * self.h class Square(Shape): def __init__(self, side): self.side = side def area(self): return self.side * self.side def total(shapes): # Any Shape is safe here — no setter surprises. return sum(s.area() for s in shapes)
The behavioral contract.
LSP is about behavior, not just method signatures. A well-behaved subtype must:
- Not strengthen preconditions (require more than the base).
- Not weaken postconditions (deliver less than the base promises).
- Preserve the base's invariants.
- Not throw new, unexpected exceptions or change expected behavior.
Square a subclass of Rectangle breaks code that sets width and height
independently.Variance and design by contract.
- Design by contract — LSP formalizes as pre/postconditions and invariants (Bertrand Meyer).
- Variance — overrides may return more specific types (covariant returns) and (in theory) accept more general parameters (contravariant), never the reverse.
- Smell — a subclass overriding a method to throw
UnsupportedOperationExceptionsignals an LSP violation. - Fix — prefer composition or a corrected hierarchy when "is-a" doesn't hold behaviorally.
Interview Questions
That objects of a superclass should be replaceable with objects of a subclass without breaking the program — subtypes must be substitutable for their base types while preserving correctness.
Barbara Liskov, in a 1987 talk (later formalized with Jeannette Wing). It became the "L" in Robert C. Martin's SOLID principles.
A Penguin subclass of Bird whose fly() throws an error, or a Square subclass of Rectangle where setting width also changes height — both break code written against the base type's expectations.
Behavior. Matching signatures (so it compiles) isn't enough; the subtype must also behave consistently with what callers expect from the base type. LSP is a behavioral/semantic contract, not just a syntactic one.
It breaks polymorphism: code using the base type can't safely rely on any subtype, forcing type checks and special cases, causing bugs, and undermining the reuse and flexibility inheritance was supposed to provide.
A subclass overriding an inherited method to do nothing or to throw UnsupportedOperationException/NotImplementedError, or callers checking the concrete type (if (x instanceof Square)) to decide behavior.
A subtype must not strengthen preconditions — it can't demand more of callers than the base does. If the base method accepts any positive number, an override requiring numbers > 100 would reject inputs the base allowed, breaking callers written to the base contract. Subtypes may weaken (accept more) preconditions, but never require more.
A subtype must not weaken postconditions — it must deliver at least what the base promises. If the base guarantees the returned list is sorted and non-null, an override returning an unsorted or null list weakens the guarantee and breaks callers relying on it. Subtypes may strengthen (promise more), but never less.
A Rectangle has independent setWidth/setHeight. Modeling Square as a subclass forces setting width to also set height (to keep it square). Code that expects Rectangle behavior — e.g. "set width to 5, height to 4, assert area == 20" — fails for a Square (it would set both to 4, area 16). The subtype breaks the base's behavioral contract, so a Square is not substitutable for a Rectangle despite the intuitive "a square is a rectangle." The lesson: mathematical is-a doesn't imply behavioral subtyping; use composition or a common immutable Shape abstraction instead.
A subtype must preserve all invariants of its base type — conditions that must always hold for base objects (e.g. "balance ≥ 0", "size == number of stored elements"). If a subclass introduces behavior that can violate a base invariant, code relying on that invariant breaks. So overriding methods and adding state must maintain everything the base guaranteed about its objects' consistent state, not just individual method contracts.
OCP relies on being able to add new subtypes that plug into existing code via the base abstraction. That only works if the new subtypes are truly substitutable — i.e. obey LSP. If a subtype violates LSP, the existing code (closed for modification) breaks when given it, so you'd have to modify it (adding type checks), defeating OCP. LSP is the correctness condition that makes OCP's polymorphic extension safe.
Re-examine the model. Options: replace inheritance with composition (a Square has dimensions rather than is a mutable Rectangle); introduce a better abstraction that captures only shared, honorable behavior (e.g. an immutable Shape with area()); split the hierarchy so subtypes don't inherit methods they can't support (e.g. Bird vs FlyingBird); or use interfaces for capabilities (Flyable) that only relevant types implement. The theme: make the inheritance reflect genuine behavioral substitutability, or don't inherit.
Generally yes, if it throws exceptions the base method's contract didn't declare or lead callers to expect, because callers written to the base won't handle them, causing failures. Subtypes should only throw exceptions that are the same as, or subtypes of, those the base contract permits. Throwing new checked/unexpected exceptions strengthens what callers must handle (akin to a precondition/contract change), violating substitutability.
Liskov & Wing's formulation: let φ(x) be 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. In other words, any property callers can rely on for the supertype must also hold for the subtype. Practically this decomposes into the contract rules: preconditions may be weakened but not strengthened, postconditions may be strengthened but not weakened, invariants must be preserved, and history constraints (how state may evolve) must be respected. It's the theoretical basis for "behavioral subtyping."
Beyond pre/postconditions and invariants, Liskov & Wing add the history rule: a subtype must not allow state changes that the base type prohibits over an object's lifetime. For example, if the base type is effectively immutable after construction (its observable state can't change), a mutable subtype violates the history constraint even if each method individually looks contract-compatible, because it permits state transitions callers of the base assumed impossible. This catches subtle violations where a subclass introduces new mutability or transitions that break assumptions clients built on the base type's allowed history of states.
Design by Contract (Meyer) specifies methods with preconditions, postconditions, and class invariants; LSP is precisely the subtyping rule for contracts: a subtype's methods may require no more (weaker/equal preconditions) and guarantee no less (stronger/equal postconditions), preserving invariants. This maps onto type variance: overrides may be covariant in return types (return a subtype) and contravariant in parameter types (accept a supertype) to remain safe — matching "promise more, demand less." Languages enforce parts of this (covariant returns in Java/C++; contravariant parameters are mostly disallowed to keep overriding simple), but the full behavioral contract (postcondition strength, invariants) is the developer's responsibility, which is why LSP is a design principle rather than something the compiler fully checks.
Mathematically a square is a special rectangle. But LSP is about behavioral substitutability of mutable objects with contracts, not set membership. A mutable Rectangle's contract includes independently settable width and height; a Square can't honor that without surprising callers. The lesson: inheritance should model shared behavior/contracts, not real-world taxonomy or shape of data. Relationships that feel like "is-a" in the domain can still be invalid subtypes if the base's mutable behavior can't be preserved. This is why immutability often rescues such hierarchies (an immutable Square/Rectangle sharing area() is fine) and why "prefer composition" is common advice when behavior diverges.
Look for concrete smells: overridden methods that throw "not supported"/do nothing; callers that instanceof/type-check to branch behavior; subclasses that ignore or contradict base parameters; overrides that tighten input validation or loosen output guarantees; and tests written for the base type failing for a subtype. A powerful technique is contract/property testing: write a test suite against the base type's contract and run it against every subtype — any subtype that fails violates LSP. Reviewing whether every method makes sense for every subtype, and whether the base's documented guarantees hold for all subtypes, surfaces violations that compilers won't catch.
Yes. LSP governs any subtype relationship where clients depend on a contract — implementing an interface, satisfying a protocol, or duck typing. If an interface implies behavioral guarantees (e.g. a Collection.add that some implementations reject), implementers that violate those expectations break clients just as subclasses would (Java's immutable collections throwing on add is a real-world LSP tension). In structurally-typed/duck-typed languages there's no compiler-declared hierarchy, but the same principle holds: any object passed where a certain behavior is expected must actually provide that behavior consistently. So LSP is really about honoring the contract of whatever abstraction callers program against, regardless of the subtyping mechanism.
Stop assuming all birds fly. Separate capabilities from taxonomy: define a base Bird with behavior all birds share (eat, layEggs), and model flight as a distinct capability — either a Flyable interface implemented only by birds that fly, or a subtype FlyingBird extends Bird that adds fly(), with Penguin extends Bird (not FlyingBird). Client code that needs flight depends on Flyable/FlyingBird, so it's never handed a penguin. Alternatively use composition (a MovementBehavior strategy: Fly, Swim, Walk) injected per bird. Either way, no subtype is forced to implement a method it can't honor, restoring substitutability and eliminating the throw-on-fly hack.
The idea that a subtype must not just have a compatible interface but also behave in a way consistent with the supertype's expected behavior. LSP is the definition of behavioral subtyping.
Only partially. The compiler checks signature compatibility (return/parameter types, that overrides exist), but it cannot verify behavioral contracts like invariants or that postconditions hold. Honoring LSP's semantics is the developer's responsibility.
That an intuitive "is-a" isn't enough to justify inheritance — the subtype must also be behaviorally substitutable. If it can't honor the base's contract, model the relationship with composition or a different abstraction instead.
When callers must query a subtype's concrete type (instanceof) before deciding what to do, they're "asking" — a sign the subtype isn't substitutable and LSP is broken. LSP encourages "telling": call the polymorphic method through the base type and trust every subtype to do the right thing. Well-behaved subtypes eliminate the need for callers to inspect concrete types, keeping behavior encapsulated in the objects.
Java's immutable collections: Collections.unmodifiableList and List.of return objects that implement the mutable List interface but throw UnsupportedOperationException on add/remove. Code written to the List contract expecting mutability breaks at runtime. This is a classic LSP violation baked into a widely-used API — arguably the interface should have separated read-only from mutable operations (an ISP fix), so that immutability wouldn't require breaking the contract.
Write a shared test suite against the base type's contract and run it for every subtype (parameterized/abstract test cases instantiated per implementation). If any subtype fails a base-contract test, it isn't substitutable. This "contract test" approach turns LSP from an abstract principle into an automated check and is especially valuable for interfaces with many implementations, catching violations the compiler can't.
Variance rules exist precisely to preserve substitutability for parameterized types. A generic type G<T> isn't automatically substitutable just because T is: List<Dog> is not safely a List<Animal> because you could add a Cat (breaking the Dog list) — mutable generics are invariant to keep LSP. Read-only producers can be covariant (? extends Animal / out T) and write-only consumers contravariant (? super Dog / in T), matching the "produce a subtype, accept a supertype" rule. So variance annotations are the type system's way of enforcing where substitution is sound, which is LSP applied to generic type parameters.
"Refused bequest" is when a subclass inherits methods/properties it doesn't want and overrides them to no-op or throw — a strong LSP smell, since the subclass can't honor part of the base contract. Fixes: (1) reshape the hierarchy so the subclass only inherits what it can support (push the unsupported behavior down to a sibling branch, e.g. split Bird into FlyingBird); (2) replace inheritance with composition/delegation so the class only exposes capabilities it actually has; (3) extract capability interfaces (ISP) so types implement only relevant ones. The underlying cure is aligning the inheritance with genuine behavioral commonality rather than convenient code reuse.
Equality is a notorious LSP trap. If a subclass adds fields and overrides equals to include them, symmetry can break: base.equals(sub) may return true while sub.equals(base) returns false, violating the equals contract (an invariant callers rely on) and thus LSP. Josh Bloch's guidance is that there's no way to extend an instantiable class with a new value component while preserving the equals contract — favor composition over inheritance for value types, or make classes final. Alternatively use a strict getClass() check (which then breaks substitutability across subclasses in a different way). This shows LSP's subtlety: even "small" overrides of universal methods can silently break the contracts that collections and callers depend on.
The same substitutability logic governs backward compatibility. A new version of an API/service that clients treat interchangeably with the old must not strengthen what it requires from callers (don't add mandatory request fields — that's strengthening preconditions) nor weaken what it guarantees (don't drop response fields or relax invariants callers depend on — that's weakening postconditions). Doing so breaks existing consumers exactly as an LSP-violating subtype breaks callers of the base. This is why compatible API evolution allows additive/optional changes (widen inputs, keep outputs at least as strong) but forbids removing/tightening — it's LSP applied across versions/contracts. Consumer-driven contract tests enforce it, mirroring class-level contract testing.