Skip to content
LLD Learn/SOLID Principles
Browsing as a guest. Sign in to save your progress and earn XP as you complete chapters.

Liskov Substitution Principle

6 min read

You'll learn to

  • -State LSP precisely: subtypes must be substitutable for their base type without breaking correctness
  • -Recognize the classic Square-extends-Rectangle trap and similar LSP violations in an interview design

The Liskov Substitution Principle (LSP) states that if S is a subtype of T, objects of type T should be replaceable with objects of type S without altering the correctness of the program. In plainer terms: any code that works with a base class should keep working, unmodified, when handed any subclass instead - no surprises, no broken assumptions.

The Square-Extends-Rectangle Trap

This is the canonical example precisely because it looks correct mathematically and fails as code. A Square genuinely is a Rectangle where width equals height. But if Rectangle exposes set_width() and set_height() independently, a Square subclass overriding both to keep width and height equal breaks any code that relied on "setting width alone changes only the width" - a reasonable assumption baked into Rectangle's contract. The subtype has silently violated a behavioral guarantee the base type promised, even though the is-a relationship is mathematically valid.

Where the substitution breaks
class Rectangle:
    def set_width(self, w): self.width = w
    def set_height(self, h): self.height = h

class Square(Rectangle):
    def set_width(self, w):
        self.width = self.height = w   # surprise: also changes height
    def set_height(self, h):
        self.width = self.height = h   # surprise: also changes width

def resize(rect: Rectangle):
    rect.set_width(5)
    rect.set_height(4)
    assert rect.width == 5   # PASSES for Rectangle, FAILS for Square
    # A Square silently breaks code that only ever depended on the
    # Rectangle contract - that's the LSP violation.

The General Pattern to Watch For

LSP violations usually show up as a subclass that strengthens preconditions (rejecting inputs the base class accepted), weakens postconditions (returning something less specific than promised), or changes an invariant the base type's callers were relying on. In interview terms: if you find yourself writing a subclass method that throws an exception the base method never threw, or that has a wider set of side effects than the base method promised, stop and check whether that subclass actually belongs in this hierarchy.

A subclass that overrides a method to throw "not supported" (like RubberDuck.fly() throwing UnsupportedOperationException) is one of the clearest LSP violations you can point to on a whiteboard - it is a subtype that cannot honor the base type's contract at all.

Interview Signal

A colleague proposes making Penguin extend Bird, where Bird has a fly() method. What is the LSP concern?

Weak Answer

"That seems fine - a penguin is technically a bird."

Strong Answer

"Biologically yes, but if Bird's contract includes fly(), any code that calls bird.fly() on a list of Birds would break or need special-casing for Penguin. That's an LSP violation - Penguin can't honor the full Bird contract. I'd either move fly() out of Bird into a separate FlyingBird interface/capability, or model Penguin so it never has to override fly() with an exception."

Check Yourself1 / 3

What does the Liskov Substitution Principle require?

ScaleDojo Logo
Initializing ScaleDojo