Skip to content

Liskov Substitution Principle

Subtypes must be substitutable for their base types without breaking the program.

The Liskov Substitution Principle (LSP) states that objects of a superclass should be replaceable with objects of its subclasses without breaking the application. In other words, derived classes must be substitutable for their base classes.

This principle was introduced by Barbara Liskov in 1987 and is a fundamental concept in object-oriented design.

Diagram

The Liskov Substitution Principle ensures that:

  • Subclasses must honor the contract of their base class
  • Subclasses should not weaken preconditions (requirements before a method runs)
  • Subclasses should not strengthen postconditions (guarantees after a method runs)
  • Subclasses should not throw new exceptions that the base class doesn’t throw

In simple terms: If it looks like a duck and quacks like a duck, it should behave like a duck!

This is a classic example that demonstrates LSP violation. Let’s see why a Square should NOT inherit from Rectangle.

Diagram
bad_shapes.py
class Rectangle:
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def set_width(self, width: float):
"""Setting width should only change width"""
self.width = width
def set_height(self, height: float):
"""Setting height should only change height"""
self.height = height
def get_area(self) -> float:
return self.width * self.height
class Square(Rectangle):
"""❌ This violates LSP!"""
def __init__(self, side: float):
super().__init__(side, side)
self.side = side
def set_width(self, width: float):
"""Problem: Setting width also changes height!"""
self.width = width
self.height = width # ❌ This breaks the contract!
self.side = width
def set_height(self, height: float):
"""Problem: Setting height also changes width!"""
self.height = height
self.width = height # ❌ This breaks the contract!
self.side = height
# This function expects Rectangle behavior
def test_rectangle(rect: Rectangle):
"""This function assumes Rectangle's contract"""
initial_area = rect.get_area()
rect.set_width(10) # Should only change width
rect.set_height(5) # Should only change height
# Expected: width=10, height=5, area=50
# But with Square: width=5, height=5, area=25 ❌
return rect.get_area()
# Usage - This breaks!
rectangle = Rectangle(5, 5)
print(test_rectangle(rectangle)) # Works: 50
square = Square(5)
print(test_rectangle(square)) # ❌ Breaks! Returns 25, not 50
Diagram
shapes.py
from abc import ABC, abstractmethod
class Shape(ABC):
"""Base class - defines the contract"""
@abstractmethod
def get_area(self) -> float:
"""Calculate and return the area"""
pass
class Rectangle(Shape):
"""Rectangle honors Shape's contract"""
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def set_width(self, width: float):
"""Only changes width - maintains contract"""
self.width = width
def set_height(self, height: float):
"""Only changes height - maintains contract"""
self.height = height
def get_area(self) -> float:
return self.width * self.height
class Square(Shape):
"""Square honors Shape's contract"""
def __init__(self, side: float):
self.side = side
def set_side(self, side: float):
"""Square-specific method"""
self.side = side
def get_area(self) -> float:
return self.side * self.side
# This function works with any Shape
def calculate_total_area(shapes: list[Shape]) -> float:
"""Works with any Shape subclass - LSP satisfied!"""
total = 0
for shape in shapes:
total += shape.get_area() # ✅ Both Rectangle and Square work!
return total

rectangle = Rectangle(5, 10) square = Square(5)

print(calculate_total_area([rectangle, square])) # ✅ Works: 75

</TabItem>
</Tabs>
**Why this follows LSP:**
- Both `Rectangle` and `Square` honor the `Shape` contract
- They can be used interchangeably where `Shape` is expected
- No unexpected behavior changes
---
## Example 2: Payment Processing System
Consider a payment processing system where different payment methods need to be processed. Let's see how LSP violations can cause real-world problems.
### Violating LSP (Bad Approach)
```d2 sketch
direction: right
before: {
label: "❌ Before (Violates LSP)"
PaymentProcessor: {
shape: class
+process_payment(amount: float)
+refund(transaction_id: str)
}
CreditCardProcessor: {
shape: class
+process_payment(amount: float)
+refund(transaction_id: str)
}
CryptocurrencyProcessor: {
shape: class
+process_payment(amount: float)
+refund(transaction_id: str)
}
PaymentProcessor -> CreditCardProcessor: extends
PaymentProcessor -> CryptocurrencyProcessor: extends
note: {
shape: text
style.italic: true
CryptocurrencyProcessor\nbreaks the contract!
}
CryptocurrencyProcessor -> note: {style.stroke-dash: 3}
}
bad_payments.py
class PaymentProcessor:
"""Base payment processor"""
def process_payment(self, amount: float) -> str:
"""Process payment and return transaction ID"""
# Base implementation
transaction_id = f"TXN-{amount}"
print(f"Processing payment of ${amount}")
return transaction_id
def refund(self, transaction_id: str) -> bool:
"""Refund a transaction - should always work"""
print(f"Refunding transaction {transaction_id}")
return True
class CreditCardProcessor(PaymentProcessor):
"""Credit card payments - follows contract"""
def process_payment(self, amount: float) -> str:
transaction_id = f"CC-{amount}"
print(f"Processing credit card payment of ${amount}")
# Process credit card...
return transaction_id
def refund(self, transaction_id: str) -> bool:
print(f"Refunding credit card transaction {transaction_id}")
# Refund credit card...
return True
class CryptocurrencyProcessor(PaymentProcessor):
"""❌ Cryptocurrency payments - violates LSP!"""
def process_payment(self, amount: float) -> str:
transaction_id = f"CRYPTO-{amount}"
print(f"Processing cryptocurrency payment of ${amount}")
# Process crypto...
return transaction_id
def refund(self, transaction_id: str) -> bool:
"""❌ Problem: Cryptocurrency refunds might not be possible!"""
raise NotImplementedError("Cryptocurrency refunds are not supported!")
# This breaks the contract - refund() should always work
# This function expects PaymentProcessor's contract
def process_refund(processor: PaymentProcessor, transaction_id: str):
"""This function assumes refund() always works"""
try:
success = processor.refund(transaction_id)
if success:
print("Refund successful!")
else:
print("Refund failed!")
except Exception as e:
print(f"Unexpected error: {e}") # ❌ Breaks when using CryptocurrencyProcessor!
# Usage - This breaks!
cc_processor = CreditCardProcessor()
crypto_processor = CryptocurrencyProcessor()
process_refund(cc_processor, "CC-100") # ✅ Works
process_refund(crypto_processor, "CRYPTO-100") # ❌ Breaks! Raises exception

Why this violates LSP:

  • Code expecting PaymentProcessor breaks with CryptocurrencyProcessor
  • refund() should always work, but crypto version throws exception
  • The contract is broken: refunds should be possible for all payment types
Diagram
payments.py
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
"""Base class - all payments can be processed"""
@abstractmethod
def process_payment(self, amount: float) -> str:
"""Process payment and return transaction ID"""
pass
class RefundableProcessor(PaymentProcessor):
"""Interface for processors that support refunds"""
@abstractmethod
def refund(self, transaction_id: str) -> bool:
"""Refund a transaction - only if processor supports it"""
pass
class CreditCardProcessor(RefundableProcessor):
"""Credit card - supports both payment and refund"""
def process_payment(self, amount: float) -> str:
transaction_id = f"CC-{amount}"
print(f"Processing credit card payment of ${amount}")
return transaction_id
def refund(self, transaction_id: str) -> bool:
print(f"Refunding credit card transaction {transaction_id}")
return True
class CryptocurrencyProcessor(PaymentProcessor):
"""Cryptocurrency - only supports payment, no refunds"""
def process_payment(self, amount: float) -> str:
transaction_id = f"CRYPTO-{amount}"
print(f"Processing cryptocurrency payment of ${amount}")
return transaction_id
# No refund() method - this is correct! Don't implement what you can't support
# Functions that work with specific contracts
def process_any_payment(processor: PaymentProcessor, amount: float):
"""Works with any PaymentProcessor"""
return processor.process_payment(amount)
def process_refund(processor: RefundableProcessor, transaction_id: str):
"""Only works with RefundableProcessor - type-safe!"""
return processor.refund(transaction_id)
# Usage - Type-safe and correct!
cc_processor = CreditCardProcessor()
crypto_processor = CryptocurrencyProcessor()
# Both can process payments
process_any_payment(cc_processor, 100) # ✅ Works
process_any_payment(crypto_processor, 100) # ✅ Works
# Only refundable processors can refund
process_refund(cc_processor, "CC-100") # ✅ Works
# process_refund(crypto_processor, "CRYPTO-100") # ✅ Type error - prevents bugs!

Why this follows LSP:

  • Each class honors its specific contract
  • CryptocurrencyProcessor doesn’t promise refunds it can’t deliver
  • Type system prevents using non-refundable processors where refunds are needed
  • No unexpected behavior or exceptions

bad_exceptions.py
class BaseClass:
def process(self, data: str):
"""Base class doesn't throw exceptions"""
return data.upper()
class DerivedClass(BaseClass):
def process(self, data: str):
"""❌ Violates LSP - throws exception base class doesn't throw"""
if not data:
raise ValueError("Data cannot be empty") # ❌ New exception!
return data.upper()
bad_return_types.py
class BaseClass:
def get_value(self) -> int:
"""Returns integer"""
return 42
class DerivedClass(BaseClass):
def get_value(self) -> str:
"""❌ Violates LSP - returns different type"""
return "42" # ❌ Should return int, not str!
bad_preconditions.py
class BaseClass:
def process(self, value: int):
"""Accepts any integer"""
if value < 0:
raise ValueError("Value must be positive")
return value * 2
class DerivedClass(BaseClass):
def process(self, value: int):
"""❌ Violates LSP - requires value > 10 (stronger precondition)"""
if value <= 10: # ❌ Weaker precondition - should accept any positive!
raise ValueError("Value must be greater than 10")
return value * 2
bad_postconditions.py
class BaseClass:
def calculate(self, x: int) -> int:
"""Returns any integer"""
return x * 2
class DerivedClass(BaseClass):
def calculate(self, x: int) -> int:
"""❌ Violates LSP - only returns even numbers (stronger postcondition)"""
result = x * 2
if result % 2 != 0: # ❌ Should accept any result!
raise ValueError("Result must be even")
return result

Remember: The Liskov Substitution Principle ensures that inheritance is used correctly and polymorphism works as expected! 🎯