Liskov Substitution Principle
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.
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.
A colleague proposes making Penguin extend Bird, where Bird has a fly() method. What is the LSP concern?
"That seems fine - a penguin is technically a bird."
"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."
What does the Liskov Substitution Principle require?