The Liskov Substitution Principle states:
If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the correctness of the program.
In this article you see a small code example in Python that violates this principle and learn why that is happening. Let’s start with valid code that has:
Calculator
with function calculate
.calculate
takes two numbers and returns the productclass Calculator():
def calculate(self, a, b): # returns a number
return a * b
calculation_results = [
Calculator().calculate(3, 4),
Calculator().calculate(5, 7),
]
print(calculation_results)
Output:
[12, 35]
Important: In the code above, the calculate function always returns a number (the product of a and b).
Every subclass of Calculator needs to implement a calculation function that returns a number. Let’s break the principle by creating a calculate function that can also raise an error. This example adds a DividerCalculator class (inherits from Calculator) where the overridden calculate function raises an error when Python tries to divide by zero.
class Calculator():
def calculate(self, a, b): # returns a number
return a * b
class DividerCalculator(Calculator):
def calculate(self, a, b): # returns a number or raises an Error
return a / b
calculation_results = [
Calculator().calculate(3, 4),
Calculator().calculate(5, 7),
DividerCalculator().calculate(3, 4),
DividerCalculator().calculate(5, 0) # 0 will cause an Error
]
print(calculation_results)
Output:
ZeroDivisionError: division by zero
There is no way to fix this code without:
What we learn here is that the DividerCalculator class is different from the Calculator class in this way:
That makes the result type different and therefore the interface different. Multiply and Divide are not the same thing when it comes to the result type and one should not derive from the other.
How can you spot suspicious code that might break the Liskov Substitution Principle? Apart from the code above, here are more bad examples:
class Line(Shape):
def calculate_surface_area(self):
return -1 # a line does not have a surface area
class Manager(Employee):
def desk_id(self):
return "" # managers usually occupy meeting rooms
class CompletedTask(Task):
def complete(self):
raise Exception("Cannot complete a completed task")
The problem is usually caused by inheriting class S from class T where S and T seem related but have one or more fundamental interface differences. You can solve this with hacks like the suspicious code above or increase or decrease the levels of abstraction in your class hierarchy.
This might lead to more code and that is fine. When your team mates start complaining about this, you can now explain to them why you took the decision. Writing correct code is more important than writing compact code.
Revisit old code and look for inherited classes with overridden functions that have confusing return values like -1, empty strings, 9999 or functions that raise exceptions. Chances are that you found a violation of the Liskov Substitution Principle.