The Liskov Substitution Principle says: if class B is a subclass of class A, we should be able to use B wherever we use A without anything breaking.
In simple language, if our code works with an Animal object, it should also work perfectly with a Dog object (since Dog extends Animal). If swapping in the subclass causes surprises — we’ve violated LSP.
Or as the joke goes: “If it looks like a duck, quacks like a duck, but needs batteries — we probably have the wrong abstraction.”
The Classic Violation: Rectangle and Square
This is the most famous LSP example. Mathematically, a square IS a rectangle. So it makes sense to write:
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
def set_width(self, width):
self._width = width
def set_height(self, height):
self._height = height
def area(self):
return self._width * self._height
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side)
def set_width(self, width):
# A square must keep sides equal -- SURPRISE!
self._width = width
self._height = width # also changes height
def set_height(self, height):
self._width = height # also changes width
self._height = height
# This function expects Rectangle behavior
def test_rectangle(rect):
rect.set_width(5)
rect.set_height(10)
assert rect.area() == 50, f"Expected 50, got {rect.area()}"
test_rectangle(Rectangle(2, 3)) # PASSES -- area is 50
test_rectangle(Square(2)) # FAILS -- area is 100!
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
setWidth(w) {
this.width = w;
}
setHeight(h) {
this.height = h;
}
area() {
return this.width * this.height;
}
}
class Square extends Rectangle {
constructor(side) {
super(side, side);
}
setWidth(w) {
this.width = w;
this.height = w; // SURPRISE -- changes height too
}
setHeight(h) {
this.width = h; // SURPRISE -- changes width too
this.height = h;
}
}
function testRectangle(rect) {
rect.setWidth(5);
rect.setHeight(10);
console.assert(rect.area() === 50, `Expected 50, got ${rect.area()}`);
}
testRectangle(new Rectangle(2, 3)); // PASSES
testRectangle(new Square(2)); // FAILS -- area is 100
class Rectangle {
protected int width, height;
Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
void setWidth(int w) { this.width = w; }
void setHeight(int h) { this.height = h; }
int area() { return width * height; }
}
class Square extends Rectangle {
Square(int side) { super(side, side); }
void setWidth(int w) {
this.width = w;
this.height = w; // SURPRISE
}
void setHeight(int h) {
this.width = h; // SURPRISE
this.height = h;
}
}
// testRectangle(new Square(2)) would fail -- area is 100, not 50
The problem: Square overrides setWidth() and setHeight() in a way that breaks the expected behavior of Rectangle. We can’t substitute a Square where a Rectangle is expected. LSP violated.
The Fix
Don’t force Square to extend Rectangle. Instead, use a common interface:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side * self.side
# Now they're siblings, not parent-child. No LSP issues.
shapes = [Rectangle(5, 10), Square(7)]
for s in shapes:
print(s.area()) # 50, 49 -- both work as expected
class Shape {
area() {
throw new Error("Must implement area()");
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(side) {
super();
this.side = side;
}
area() {
return this.side * this.side;
}
}
// Siblings, not parent-child. Both are Shapes.
[new Rectangle(5, 10), new Square(7)].forEach((s) =>
console.log(s.area())
); // 50, 49
interface Shape {
int area();
}
class Rectangle implements Shape {
int width, height;
Rectangle(int w, int h) { width = w; height = h; }
public int area() { return width * height; }
}
class Square implements Shape {
int side;
Square(int s) { side = s; }
public int area() { return side * side; }
}
// Both implement Shape. No parent-child weirdness.
Another Example: Birds That Can’t Fly
# BAD -- Penguin violates LSP
class Bird:
def fly(self):
print("Flying high!")
class Penguin(Bird):
def fly(self):
raise Exception("Penguins can't fly!") # SURPRISE!
# GOOD -- separate the ability to fly
class Bird:
def eat(self):
print("Eating...")
class FlyingBird(Bird):
def fly(self):
print("Flying high!")
class Penguin(Bird):
def swim(self):
print("Swimming!")
# Now nobody expects Penguin to fly
// BAD -- Penguin violates LSP
class Bird {
fly() {
console.log("Flying high!");
}
}
class Penguin extends Bird {
fly() {
throw new Error("Penguins can't fly!"); // SURPRISE!
}
}
// GOOD -- separate the ability to fly
class Bird {
eat() {
console.log("Eating...");
}
}
class FlyingBird extends Bird {
fly() {
console.log("Flying high!");
}
}
class Penguin extends Bird {
swim() {
console.log("Swimming!");
}
}
// BAD -- Penguin violates LSP
class Bird {
void fly() { System.out.println("Flying high!"); }
}
class Penguin extends Bird {
void fly() { throw new RuntimeException("Can't fly!"); }
}
// GOOD -- separate the concerns
class Bird {
void eat() { System.out.println("Eating..."); }
}
class FlyingBird extends Bird {
void fly() { System.out.println("Flying high!"); }
}
class Penguin extends Bird {
void swim() { System.out.println("Swimming!"); }
}
How to Detect LSP Violations
Watch out for these code smells:
- Subclass throws exceptions the parent doesn’t —
fly()throws “can’t fly” - Subclass does nothing in an inherited method — empty override of
save() - Subclass changes the expected behavior — setting width also sets height
- We check the type before calling a method —
if isinstance(bird, Penguin): skip fly()
If we find ourselves writing instanceof or typeof checks to handle subclass differences, that’s a strong hint our hierarchy is wrong.
The Simple Rule
Every subclass should strengthen the parent’s promises, never weaken them. If the parent says “I can fly,” every child better be able to fly. If not, we need a different abstraction.