Card 05/ 09

ExampleDifficulty: Intermediate2 min

Liskov Example: Why a Square Is Not a Rectangle in Code

Set the width to 5 and the height to 4, and the area comes back 16.

A square is a rectangle in geometry, so making Square a subclass of Rectangle looks right. This example shows why it breaks substitution in code: a rectangle's width and height can change independently, and a square's cannot. Predict the result before you read the output.

Rectangle has set_width, set_height and area. Square keeps its sides equal by overriding both setters to set both sides. The function resize_and_measure was written for rectangles: it sets the width to 5, then the height to 4, and returns the area.

Predict: what does resize_and_measure return for a Rectangle, and for a Square?

A rectangle returns 20, which is what the function's author expected. A square returns 16: setting the height to 4 also set the width to 4, undoing the first call.

py
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):
        self.width = width
        self.height = width

    def set_height(self, height):
        self.width = height
        self.height = height


def resize_and_measure(shape):
    shape.set_width(5)
    shape.set_height(4)
    return shape.area()


print(f"Rectangle: {resize_and_measure(Rectangle(1, 1))}")
print(f"Square:    {resize_and_measure(Square(1))}")
OutputRan with python:3.12-slim (Python 3.12.13)
Rectangle: 20
Square:    16
Square's setters keep the sides equal, which breaks what the caller assumed.

Every line of Square is correct for a square. The failure is in the relationship. Rectangle carried an unwritten invariant: setting one side leaves the other alone. Any code that resizes a rectangle relies on it. Square cannot keep that invariant and stay a square, so it breaks the third of the Liskov rules.

The lesson is that "is a" in the real world does not decide inheritance in code. What decides it is whether the subtype can keep the parent's behaviour, and here it depends on the parent being mutable. If shapes were immutable, with no setters, a square could be a rectangle safely, because there would be no independent resize for it to break. A common fix is exactly that: make shapes immutable, or make Square and Rectangle siblings under a Shape that only promises area.