LSP: The Liskov Substitution Principle
Subtypes must be substitutable for their base types — LSP violations at the architectural level force expensive special-case logic into the design.
Why this matters
Barbara Liskov's substitution rule states that if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the correctness of the program. This is not just a class-level rule — Martin shows how LSP violations ripple up to the architectural level.
The architecture example involves a taxi dispatch service: if two taxi companies expose slightly different REST interfaces, the dispatch system must add a special case for one of them. That special case is an LSP violation at the service boundary. Similarly, the classic Rectangle/Square trap shows how a subclass that silently changes parent behavior breaks any code that holds a reference to the parent type. The rule: if you must write if isinstance(obj, Subclass) before using it, the substitution principle has been violated and your design will accumulate dispatch logic over time.
✗The problem
Square extends Rectangle but silently keeps both sides equal — code that widens a Rectangle produces wrong area when passed a Square.
Bad
class Rectangle:
def __init__(self, w: float, h: float):
self.width = w
self.height = h
def set_width(self, w: float) -> None: self.width = w
def set_height(self, h: float) -> None: self.height = h
def area(self) -> float: return self.width * self.height
class Square(Rectangle): # LSP violation
def set_width(self, w: float) -> None:
self.width = self.height = w # silently overrides both!
def set_height(self, h: float) -> None:
self.width = self.height = h
def make_wider(r: Rectangle) -> None:
r.set_width(r.height * 2)
# Postcondition: width == height * 2 — FALSE when r is a Square
class Rectangle {
constructor(protected width: number, protected height: number) {}
setWidth(w: number): void { this.width = w; }
setHeight(h: number): void { this.height = h; }
area(): number { return this.width * this.height; }
}
class Square extends Rectangle { // LSP violation
setWidth(w: number): void { this.width = this.height = w; }
setHeight(h: number): void { this.width = this.height = h; }
}
function makeWider(r: Rectangle): void {
r.setWidth(r["height"] * 2);
// Postcondition violated when r is actually a Square
}
✓The solution
Rectangle and Square both implement a common Shape interface independently — no problematic inheritance relationship between them.
Good
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
class Rectangle(Shape):
def __init__(self, w: float, h: float):
self._width = w
self._height = h
def set_width(self, w: float) -> None: self._width = w
def set_height(self, h: float) -> None: self._height = h
def area(self) -> float: return self._width * self._height
class Square(Shape): # independent — no broken inheritance
def __init__(self, side: float):
self._side = side
def set_side(self, s: float) -> None: self._side = s
def area(self) -> float: return self._side ** 2
interface Shape {
area(): number;
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
setWidth(w: number): void { this.width = w; }
setHeight(h: number): void { this.height = h; }
area(): number { return this.width * this.height; }
}
class Square implements Shape { // independent, no broken inheritance
constructor(private side: number) {}
setSide(s: number): void { this.side = s; }
area(): number { return this.side ** 2; }
}
💡Key takeaway
If you need to check the type of an object before using it, LSP is violated — and your architecture will accumulate expensive special-case dispatch logic every time a new subtype is added.
🔧 Some exercises may still have errors. If something seems wrong, use the Feedback button (bottom-right of the page) to report it — it helps us fix it fast.
Hint: If you need to check the type of an object before using it, LSP is violated.
✗ Your version