Course contents

Liskov Substitution Principle

Sign in to run this lab and save your progress

Learn

Before you start

LSP is the SOLID principle that takes the sharpest read. The formal statement (Liskov + Wing, 1994) is a paragraph of type theory. The everyday version:

A child class must work anywhere the parent works, without surprising the caller.

Three checks

To see if Child really is a special kind of Parent, ask:

  1. Promise check. Does Child promise everything Parent promised, and no less? (Overriding a method to raise "not implemented" breaks this.)
  2. No-surprise check. Does existing code that used Parent keep working when given a Child? (Square breaks this for Rectangle.)
  3. Rule check. Does Child keep every rule Parent promised? (If Parent sorts a list, Child shouldn't return it in reverse.)

If any answer is "no", you have an LSP problem — usually solved by making the classes siblings under a common parent instead of parent-and-child.

Why Rectangle-Square is famous

It's the tightest possible example against "is-a" thinking:

  • In math, every square IS a rectangle.
  • In code, Square(Rectangle) breaks callers.

Why? Because inheritance in code copies BEHAVIOR, not just math-style membership. Rectangle's behavior is "you can change width and height on their own". Square can't keep that behavior AND stay a square. The child-parent relationship fails on behavior.

Lesson: "is-a in English" is not enough. The behavior of the parent has to make sense for every child.

The three-question checklist for interviews

Before writing class B(A):, ask:

  1. Can B do everything A can, without shrinking anything?
  2. Can existing users of A safely be handed a B?
  3. Do B's overrides keep A's promises?

Any "no" → use composition or sibling classes instead.

About the structural tests in this lesson

test_square_does_not_inherit_from_rectangle and test_square_has_no_set_width are strict checks. They pass the correct design and fail the tempting-but-broken one. If you copy the pre-fix code (Square inherits from Rectangle) they fail immediately — a guardrail against sliding back into the smell.

Where this shows up in design interviews

Almost every hierarchy in a design problem has an LSP trap:

  • Chess — is KnightMove a Move? Yes, if all Move promises hold. No, if KnightMove.can_apply(from, to) narrows what Move accepts.
  • File system — is Directory a File? No — files have content; directories have children. Make them siblings under Node.
  • Parking spots — is CompactSpot a RegularSpot? Only if every regular spot's promise holds. If not, use siblings.

Interviewers who care about design will ask "why does X inherit from Y?" as a quiet LSP probe. Answer with the three-question checklist and you'll ace it.

Your task

L in SOLID. Named after Barbara Liskov, whose 1987 paper gave the formal version. The everyday version:

If Child is a subclass of Parent, code that expects a Parent should keep working when you hand it a Child.

In plainer words: a child class must not surprise code that thinks it's using the parent.

The classic bad example: Square inherits from Rectangle

This is the LSP example every book uses. Watch what goes wrong.

class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height

    def set_width(self, w):  self._width = w
    def set_height(self, h): self._height = h
    def area(self):          return self._width * self._height


class Square(Rectangle):
    def __init__(self, side):
        super().__init__(side, side)

    # A square's width and height must stay equal — so overriding
    # the setters to keep them in sync seems reasonable:
    def set_width(self, w):
        self._width = w
        self._height = w

    def set_height(self, h):
        self._width = h
        self._height = h

Looks fine? Now write code against Rectangle:

def resize(r: Rectangle):
    r.set_width(5)
    r.set_height(4)
    return r.area()

resize(Rectangle(1, 1))   # 20 — correct, 5 * 4
resize(Square(1))         # 16 — WRONG. Caller expected 20.

Square broke the caller's assumption. Setting width was supposed to leave height alone. Square breaks LSP: you can't use it in place of Rectangle without changing behavior.

Why this matters in real code

Every inheritance choice faces this question. If a child class weakens a promise the parent made — "the setters are separate", "the method never raises an error", "iteration returns items in the order they were added" — every caller that relied on the promise will silently break the moment a child appears.

Signs of LSP problems in real production code:

  • Child classes with methods that raise "not implemented".
  • try: parent_method(); except SubclassError: ... at every call site.
  • "Check the type first" logic scattered everywhere.

All three are LSP screaming for help.

The fix: don't force the hierarchy

Rectangle and Square are BOTH shapes, but neither is a special kind of the other. Make them siblings under a common parent instead:

class Shape(ABC):
    @abstractmethod
    def area(self): ...

class Rectangle(Shape):
    def __init__(self, width, height):
        self._width = width
        self._height = height
    def set_width(self, w): self._width = w
    def set_height(self, h): self._height = h
    def area(self): return self._width * self._height

class Square(Shape):
    def __init__(self, side):
        self._side = side
    def set_side(self, s): self._side = s
    def area(self): return self._side * self._side

Now resize() operates on Rectangles only. Callers who need to work with Squares call set_side — a different method, no ambiguity, no surprise.

Your task

Build the FIXED design in main.py:

  1. Abstract Shape(ABC) with abstract area() returning a number.
  2. Rectangle(Shape):
    • __init__(self, width, height) stores both
    • set_width(self, w) sets width only
    • set_height(self, h) sets height only
    • area() returns width * height
  3. Square(Shape):
    • __init__(self, side) stores side
    • set_side(self, s) sets side
    • area() returns side * side
  4. Neither Rectangle nor Square inherits from the other.

Tests verify each class independently, verify that resize() (defined in the tests, works on Rectangles only) gives the right answer, and verify structurally that Square is NOT a subclass of Rectangle.

Click Submit to run the tests.

Starter files

Preview only — open the lab to edit and run.

files_dir
03-liskov-substitution-starter