Liskov substitution
A subclass must be usable wherever its parent is.
01The intuition
The problem it solves
You have a Rectangle with a width and a height, and a function that tests it. Then someone says a square is a rectangle, so Square inherits from Rectangle and keeps its sides equal.
class Rectangle:
def __init__(self, w, h): self.w, self.h = w, h
def set_w(self, w): self.w = w
def set_h(self, h): self.h = h
def area(self): return self.w * self.h
class Square(Rectangle):
def set_w(self, w): self.w = w; self.h = w
def set_h(self, h): self.w = h; self.h = h
def stretch(r: Rectangle):
r.set_w(4); r.set_h(5)
assert r.area() == 20 # fails for Square: area is 25
stretch was written against the promise of Rectangle: setting width does not touch height. Square is a rectangle in geometry but breaks that promise in code, so any function that worked with rectangles can now silently fail. The same bug appears in interviews as a ReadOnlyList that inherits from List and throws on add, or a Penguin that inherits fly and raises.
The Liskov substitution principle says: wherever the code expects the parent, you must be able to hand it any child and nothing observable changes except for the better.
Build it one piece at a time
Step 1: write down the parent's promises. Not the method names, the behaviour. For Rectangle: "after set_w(x), w == x and h is unchanged". A child must keep every such sentence true.
Step 2: check each child against the list. Square.set_w changes h, so Square fails. The fix is not a cleverer override; it is to stop pretending.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
class Rectangle(Shape):
def __init__(self, w: float, h: float): self.w, self.h = w, h
def area(self) -> float: return self.w * self.h
class Square(Shape):
def __init__(self, side: float): self.side = side
def area(self) -> float: return self.side ** 2
Step 3: move shared behaviour to the promise both can keep. Both really can promise area. Neither needs to promise independent setters, so that promise is dropped from the shared parent and the stretch function takes a Rectangle explicitly.
Step 4: when you do subclass, only strengthen what you give and only loosen what you demand. A child may accept more inputs and return more specific results, never fewer or vaguer.
class Parser:
def parse(self, text: str) -> dict:
if not text: raise ValueError("empty")
return {"raw": text}
class LenientParser(Parser):
def parse(self, text: str) -> dict: # accepts more: empty is fine
if not text: return {}
return {"raw": text.strip()} # returns a dict as promised
class StrictParser(Parser):
def parse(self, text: str) -> dict: # breaks LSP: demands more
if len(text) < 3: raise ValueError("too short")
return {"raw": text}
| Child | Inputs it accepts | Promises it keeps | Substitutable |
|---|---|---|---|
| Square(Rectangle) with linked setters | same | breaks "h unchanged" | no |
| LenientParser | more (empty ok) | all, returns a dict | yes |
| StrictParser | fewer (rejects short) | breaks "non-empty parses" | no |
Why it works
Callers are written against the parent's contract and never read the child's code. If every child keeps the contract, a caller that is correct for the parent is correct for all children, which is the whole reason polymorphism is safe. The invariant has three parts, worth memorising: a child's method may require no more than the parent (preconditions not strengthened), must guarantee at least what the parent does (postconditions not weakened), and must keep the parent's invariants.
Where it breaks in practice: overriding a method to throw NotImplementedError, overriding to do nothing, and type checks like isinstance(x, Square) inside code that takes a Rectangle. Each one is a sign that the thing is not really a subtype and should be composed instead; see inheritance versus composition.
The trade-off you must say out loud
Obeying Liskov sometimes means a shallower hierarchy than the real-world taxonomy suggests, and a few duplicated lines, which feels wrong to people who learned that inheritance exists to remove duplication. Say the rule: "I inherit only when the child keeps every promise of the parent; otherwise I share an interface or compose". The follow-up is usually a concrete trap, "can an ElectricCar extend Car", and the right move is to ask which Car methods a caller relies on and whether the child can honour each.
Where it shows up in the 100 Google problems
- Design chess: every
Piecepromiseslegal_moves(board)returns only moves that keep the king safe; a knight that returned raw squares would break check detection for the whole board. - Design a deck of cards and Blackjack: a
Dealerthat is aPlayermust still honourtake_turn; its fixed hit-until-17 rule narrows behaviour without breaking any caller. - Design Monopoly: each
Squaresubclass must completeon_landwithout leaving the player in an invalid state, or the turn loop cannot trust any square. - Design a multi-elevator dispatcher: a maintenance-mode car is still an
Elevatorthat answers "can you take this call" with no, rather than one whoseassignraises.