Pop quiz: Is a square a rectangle? Mathematically, yes. In code? That’s where things get spicy. Welcome to the Liskov Substitution Principle — the SOLID rule that’ll make you question everything you learned in geometry class.
Imagine you’re building with LEGO. You expect any 2x4 brick to work wherever you need a 2x4 brick. Now imagine grabbing what looks like a 2x4 brick, but when you place it, it suddenly becomes a 4x4 brick and breaks your entire structure. That’s what LSP violations feel like — except with more swearing and late-night debugging.
🤔 Why Should You Care About LSP?
Without LSP:
- Substituting child classes breaks parent functionality
- You need endless type checking and if-statements
- Code becomes fragile and unpredictable
- Testing becomes a nightmare
With LSP:
- Any child class works seamlessly in place of parent
- No surprise behavior changes
- Polymorphism actually works as intended
- Code stays reliable and maintainable
Think of it as the “no surprises” rule — your subclasses should behave predictably.
🔄 Liskov Substitution Principle: The Golden Rule
The Rule: If you have a parent class, any child class should be able to replace it without breaking things.
Translation: Child classes must honor the “contract” established by the parent class. No sneaky behavior changes allowed.
🚨 The Classic Trap: Square vs Rectangle
Let’s walk through the most famous LSP violation in programming history.
Setting Up the Rectangle
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
@property
def area(self):
return self._width * self._height
@property
def width(self):
return self._width
@width.setter
def width(self, value):
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
self._height = value
So far so good. Rectangle has width, height, and can calculate area. Simple stuff.
Adding Square (The Trap!)
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side)
@Rectangle.width.setter
def width(self, value):
self._width = value
self._height = value # Keep sides equal!
@Rectangle.height.setter
def height(self, value):
self._height = value
self._width = value # Keep sides equal!
Seems logical, right? Squares have equal sides, so when you set width, you must also set height. Perfect!
Wrong. Watch what happens next.
💥 Where It All Falls Apart
def calculate_area(shape):
width = shape.width
shape.height = 10
expected = width * 10
print(f'Expected: {expected}, Got: {shape.area}')
# Test with Rectangle
rect = Rectangle(5, 20)
calculate_area(rect)
# Output: Expected: 50, Got: 50 ✅
# Test with Square
sq = Square(5)
calculate_area(sq)
# Output: Expected: 50, Got: 100 ❌ BROKEN!
What happened?
- Square starts with width=5, height=5
- We store width (5) in a variable
- We set height to 10
- But Square’s setter ALSO changes width to 10!
- We expected 5 × 10 = 50
- We got 10 × 10 = 100
LSP VIOLATION: You can’t substitute Square for Rectangle without breaking behavior. The function expects setting height won’t affect width. Square broke that contract.
🧠 Why This Matters in Real Code
This isn’t just a geometry problem. Here are real scenarios:
Database connections:
class DatabaseConnection:
def connect(self):
# Establish connection
pass
class ReadOnlyConnection(DatabaseConnection):
def connect(self):
# Connects, but write() throws errors!
pass
If code expects DatabaseConnection
to support writes, ReadOnlyConnection
breaks LSP.
File handlers:
class FileHandler:
def save(self, data):
# Saves to file
pass
class NetworkFileHandler(FileHandler):
def save(self, data):
# Might fail if network is down!
pass
If FileHandler
guarantees saves work, NetworkFileHandler
violates LSP by potentially failing.
✅ How to Fix LSP Violations
Option 1: Don’t Use Inheritance
# Instead of Square inheriting Rectangle
class Shape:
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 Square and Rectangle are siblings, not parent-child. No LSP violation!
Option 2: Composition Over Inheritance
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
class Square:
def __init__(self, side):
self._rectangle = Rectangle(side, side)
@property
def area(self):
return self._rectangle.area
Square uses Rectangle internally without inheriting from it.
💡 Real-World LSP Guidelines
Ask these questions:
- Can I substitute child for parent everywhere?
If no → LSP violation - Does child class strengthen or weaken parent contracts?
Weaken → LSP violation - Does child throw new exceptions parent didn’t?
Yes → LSP violation - Does child require more preconditions than parent?
Yes → LSP violation
Real-world analogy: Car keys. Any BMW key should work in any BMW. If a “special BMW key” only works in some BMWs, that’s an LSP violation.
🎯 Quick Wins
- Be cautious with inheritance — Just because something IS-A doesn’t mean it should inherit
- Favor composition — “Has-a” often beats “is-a”
- Test substitutability — Run parent tests on child classes
- Watch for type checking — If you need
isinstance()
checks, you might have LSP issues
📚 TLDR Cheat Sheet
✅ LSP Rule: Child classes must work wherever parent classes work
✅ Square Problem: Inheritance can violate expected behavior
✅ Fix: Use composition or separate hierarchies
✅ Red Flag: Type checking with isinstance()
✅ Golden Question: Can I substitute without surprises?
That’s LSP! It’s the principle that keeps your inheritance hierarchies honest and your polymorphism predictable. Next time you reach for inheritance, ask: “Will this behave predictably everywhere?” If not, composition might be your friend. 🚀
Got LSP questions or horror stories? Share below! 💻✨