Liskov Substitution Principle
Subtypes must be usable wherever their base type is expected.
Intent
If a function accepts a Rectangle, passing a Square (when Square is-a Rectangle) must not break the function’s assumptions.
Problem
It feels natural: a square is a rectangle. So Square subclasses Rectangle and overrides setters so sides stay equal.
But callers of Rectangle assume setting height only changes height. Square also rewrites width. The contract breaks.
Violation (this example)
use_it saves width, sets height = 10, expects area == width * 10.
Structure
| Piece | Role |
|---|---|
Rectangle | Independent width / height |
Square | Setters keep sides equal (breaks parent contract) |
use_it | Client that trusts Rectangle behavior |
Compare code
Subtype changes the meaning of setters
class Square(Rectangle):
@Rectangle.width.setter
def width(self, value):
self._width = self._height = value
@Rectangle.height.setter
def height(self, value):
self._height = self._width = valueClient assumes Rectangle rules
def use_it(rc: Rectangle):
w = rc.width
rc.height = 10
expected = int(w * 10)
print(f"Expected: {expected}, Actual: {rc.area}")Walkthrough
Quick check
Why does Square break LSP here?
Run locally
uv run lspExpected shape:
Expected: 20, Actual: 20
Expected: 50, Actual: 100Source: main.py · tutorial README
Takeaway
- Inheritance is a behavioral promise, not just reuse
- If the subtype can’t honor the parent contract, don’t inherit that way
- Prefer a shared
Shape(or similar) over forcing Square into Rectangle
Better direction
Don’t model a mutable square as a rectangle subtype — keep them as siblings under a common abstraction, or drop independent setters on a shared base.