Liskov Substitution Principle
A subclass must keep every promise its parent makes, so code written for the parent works unchanged with the child.
Big idea
The Liskov Substitution Principle (the L in SOLID): anywhere you use a parent type, you must be able to drop in any subtype without the program going wrong. A subclass may do more, but it may not demand more from callers or promise less to them.
The problem it solves
The textbook trap: a Square is a rectangle in geometry, so Square(Rectangle) seems natural. A square must keep width and height equal, so its set_width also sets the height. Then this perfectly reasonable function, written for rectangles, breaks:
def stretch(r):
r.set_width(5)
r.set_height(4)
assert r.area() == 20 # fails for a Square: area is 16
The code wasn't wrong; the inheritance was. "Is-a" in the real world isn't the test — behaving like one is.
Watch it
Think of it like
A hire-car booking for "a mid-size car". You'll accept any model — that's substitution. But if they hand you one that only runs on a fuel no station sells, or won't go above 30 km/h, it technically is a car and it's useless to you. A substitute has to work everywhere the original was promised to work.
The rules a subtype must keep
- Preconditions can't be strengthened — if the parent accepts any positive amount, the child can't demand amounts over 10.
- Postconditions can't be weakened — if the parent promises a sorted list, the child must return one too.
- Invariants must hold — whatever is always true of the parent stays true.
- No new surprise exceptions — callers handling the parent's errors shouldn't meet new ones.
- History rule — a child can't allow state changes the parent forbids (an immutable parent can't have a mutable child).
Square and Rectangle become siblings: both are shapes, neither promises the other's resize behaviour.
Hands on
ReadOnlyFile subclasses File but throws where the parent promised to write. The backup routine — written against File — crashes. Restructure so read-only files aren't passed where writable ones are promised (one clean way: a Readable base with read(), and File adding write()), keeping the printed output shown.
class File:
def __init__(self, name, text=""):
self.name, self.text = name, text
def read(self):
return self.text
def write(self, text):
self.text = text
class ReadOnlyFile(File):
def write(self, text):
raise PermissionError(f"{self.name} is read-only")
def print_all(files):
for f in files:
print(f"{f.name}: {f.read()}")
def stamp_all(files): # written against File: "every file can be written"
for f in files:
f.write(f.read() + " [backed up]")
notes = File("notes.txt", "buy milk")
licence = ReadOnlyFile("LICENSE", "MIT")
print_all([notes, licence])
stamp_all([notes, licence])
print_all([notes])output
notes.txt: buy milk
LICENSE: MIT
notes.txt: buy milk [backed up]Run and edit it in the app.
Parent: withdraw(amount) accepts any amount up to the balance. Child: withdraw(amount) raises if amount > 500, even with enough balance. What's wrong?
Smells that point to an LSP break
- A subclass method that raises
NotImplementedErroror does nothing ("refused bequest"). - Callers doing
isinstancechecks to special-case one subtype. - Overrides whose docs say "unlike the parent, this…".
When it matters most
Whenever someone else's code holds a reference to your parent type: plug-ins, frameworks, collections of mixed subtypes, anything behind an interface. If no code ever treats the child as the parent, you're not using inheritance for substitution — consider composition instead.
Pros and cons of honouring it
| Pros | Cons |
|---|---|
| Polymorphic code just works with new subtypes | Forces you to rethink "obvious" hierarchies |
No isinstance special cases | Sometimes means flatter, sibling hierarchies |
| Contracts become explicit | Requires thinking in pre/postconditions |
How it relates
- It's what makes Open/Closed safe: plugged-in subtypes must honour the contract.
- Interface Segregation often fixes LSP breaks by splitting the promise (as
Readable/Filedid above). - The smell Refused Bequest in OO Abusers is an LSP violation in disguise.
You find "if isinstance(bird, Penguin): skip" before calling bird.fly(). What does that most likely indicate?
Key terms
- Liskov Substitution Principle — Subtypes must be usable anywhere their parent type is expected, without surprises.
- Precondition — What must be true before a method is called; a subtype may not demand more.
- Postcondition — What a method guarantees afterwards; a subtype may not guarantee less.
- Refused bequest — A subclass that rejects or disables behaviour it inherited.