The Liskov Substitution Principle (LSP) Explained in Python.

The Liskov Substitution Principle (LSP) Explained in Python.
Photo by Aaron Burden / Unsplash

Towards robust software with SOLID principle 3/5.

This post is part 3 of a series on the SOLID principles.
You can find the second post here.

Before we delve into the reasoning of this principle and how it fits in with the last two principles, let's look at the definition.

The Liskov substitution principle is formally defined as:

Let ϕ(x) be a property provable about objects x of type T. Then ϕ(y) should be true for objects y of type S where S is a subtype of T. Source: Barbara Liskov

Let's face it, this makes no sense to the modern software engineer 😂

Here's a simpler definition:

It should be possible to replace an instance of a superclass with an instance of a subclass without causing breaking changes.

Say you have a subclass which derives from a base class/superclass (inheritance). Let's say you make some changes to the subclass such as change of parameter value types and perhaps also the return type. In this case you would have violated the LSP principle because replacing an instance of the superclass with an instance of the subclass would lead to breaking changes. The code that depends on the base class would expect a return type of str for example but when you replace the instance with the subclass, it returns an int . This would could cause the code to crash or raise other errors.

In short, the behavior of the base class must conform to the behavior of the superclass. In general, the function signature (function parameters) and return type must be unchanged in the subclass.

The main purpose of this principle is to ensure that old codebases do not break when new code is introduced. Furthermore, it ensures flexible code.

Code

Let's look at a simple example. We change the scenario slightly from previous posts by considering a car rather than a car dealership😉:

The Car class defines a car. We initialize it with a name, the number of gears, the speed and the current gear position. The changeGear function allows changing of gears while the accelerate function increases the speed of the car by 1. If the gear is in the neutral position N we don't change the speed but inform the user instead.
Pretty simple.

SportsCar inheriting Car. Source: Author.

Let's say we want to model a SportsCar also. We make it inherit from the base class Car. At initialization we also define a turbos variable which is a list showing what turbo levels the car supports. Because of the behavior change we need to define a new acceleration function that takes turbo as a parameter. The speed is increased by the turbo amount instead of 1 so the sports car can accelerate faster.

In the __main__ function we define a normal car.

Here's the problem:

If we try to replace the Car instance this with an instance of a SportsCar it causes the code to break since accelerate in SportsCar expects a turbo argument.

By LSP, replacing the above should not give an error and the code should function as normal.

There are two ways to go about this.

Solution 1

A simple solution would be to replace the turbos variable with a turbo variable having a fixed value. And removing the parameter turbo from the accelerate function like so:

As you can see, the function signatures are now directly identical to the base class function signatures. This means we can replace the Car code in the __main__ function with SportsCar without code breakage. Yay!

However, if you notice we have a fixed turbo value 2 in this case.
What if we want the user to define the turbo value like it was possible before?
This requires using an abstract class. On we go!

Solution 2 - Abstract class

The issue is the way we have modelled the cars. To make the code above conform to the LSP and have the functionality of turbo choice we must create an abstract base class Car. Then we create two separate subclasses that inherit from it namely: SportsCar and RegularCar.  The diagram illustrates this:

Car is abstract. RegularCar and SportsCar implement the abstract class. Source: Author.

It'll make more sense with the code:

We transform Car to an abstract class using the ABC module. The details of this are not important. The init function is annoated with @abstractmethod to denote an abstract function. This ensures that a Car cannot be instantiated. Only classes that inherit from this can be instantiated.

Next, we create two classes that inherit from Car:

  • RegularCar indicating a normal car without turbo
  • SportsCar indicating a sports car with turbo

The RegularCar has no additional changes.
In the SportsCar class we can make the modifications we desire. We define the turbos array and the turboAccelerate function containing the extra turbo parameter.

In this case, changing the function signature in SportsCar is not violating the LSP. Why? Because the base class Car is abstract so it cannot be instantiated.
If it cannot be instantiated, it cannot be replaced.

Conclusion

In this post we looked at the Liskov substitution principle and what it means.
We saw an example violating the principle and solve it using two methods.

LSP helps makes code flexible and prevents issues when old code is extended with new code. LSP ensures that behavior of code remains the same across both new and old code bases, resulting in code that is less prone to breakage.
Overall, code robustness increases as a result.

This post was a bit more involved than previous posts especially since we approached the problem in a roundabout way. I wanted to explore an interesting scenario which increased complexity slightly. I hope you enjoyed the content!


Enjoyed this article? Please consider subscribing using the form below😇
It is 100% free. You get an email every time I post (about once a week) + access to premium content in the future.