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.