Liskov's Substitution Principle Explained Using Python

Liskov's substitution principle is a part of the S.O.L.I.D design principle where L is for Liskov's substitution principle. You might be wondering if this sounds like some mathematical principle in computer science that is hardly used in the daily life of software developers.

Nope! my friend, you cannot be more wrong than this. Liskov's substitution principle actually helps you write better code where inheritance is used. It actually alters your thinking of how you actually do inheritance in your code.

We are used to writing inheritance code by thinking that if there is some shared piece of code & that has some similar behavior that is can be extended by other classes then create a parent class with the code & behavior & you are done for the day!

If this sounds like your code then there is a better way to write parent-child classes using the concept of inheritance. Liskov's principle is nothing but a way to validate if the inheritance pattern you are using is valid conceptually or not. You can still have fully functional inherited code but it might not be conceptually correct & prone to break with a new set of requirements.

Too much talking!!! Let's take a look at the actual definition,

LSP was given by Barbara Liskov in a 1988 conference keynote address titled Data abstraction and hierarchy, According to Wikipedia definition is,

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.

Sigh, Too much mathematics for the day!!

Hang on there! Let me break it down into pure English sentences. This basically translates to,

The objects of a superclass or base class can be easily replaced by objects of its subclasses or sub-type without breaking the application.

For example, Suppose your friend has a duck as a pet, somehow that pet has died and your friend is sad & to make him happy again you cannot try to replace that pet duck with a plastic duck toy, it may quack like Duck & float on water like Duck but still it is not a real Duck at all & your friend won't be happy!. This is the LSP in the super simplified example.

Let's understand this by using abstraction classes, Suppose you are building software to draw animals on the screen for kids to learn about animals, So you start with a few animals like lions, cheetahs, foxes, horses, donkeys etc, Now you will have to model these animals in the code, So you start thinking by creating a base abstract class Animal like this,

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def run(self):
          pass # Make animal run on the screen

Now you will use this abstract base class for creating different animals such as lions & horses like this,

class Horse(Animal):

        def run(self):
              print("Horse is running....")

Now you create a client code to use these animal objects to draw them on the screen.

class Screen:
    def draw(animal: Animal):
        animal.run()


if __name__ == '__main__':

    s = Screen()

    s.draw(Horse())

Now, this looks like perfectly fine code with no issues so far, right? what will happen when the new requirements come in? Assume your software getting popular and demand is increasing, users are demanding to include a few more animals like rabbits, wolves, eagles & goldfish, etc.

So you start implementing these animals as well,

class Eagle(Animal):

    def run(self):
        # Wait eagle don't run!
        raise NotImpelementedError()

    def fly(self):
          print("Eagle is flying...")

This is a classical case of violating the LSP. There are a few problems with this approach, First is that you cannot replace the instance of Animal in the draw method even though it extends the Animal class & it violates the LSP as it states that every subtype of Animal can be replaced with it without breaking the application. The second problem is that it raises an exception in the implemented run method which is also prohibited in the LSP. We will talk about this later.

So LSP actually forces you to think if the model of your environment you are building can handle every case of future requirements & your code can be extended without breaking a sweat.

Let me give you another example to make it clear,

from abc import ABC, abstractmethod

class TransporationDevice(ABC):
        @abstractmethod
        def start_engine(self):
            pass

class Car(TransporationDevice):
        def start_engine(self):
           print("Starting Engine...")

class Bike(TransporationDevice):
        def start_engine(self):
           print("Starting Engine...")

class Cycle(TransporationDevice):
        def start_engine(self):
          # Bicyles don't have engines
          raise NotImplementedError()

Here the Cycle is a transportation device but it does not have an engine to start. It clearly breaks the LSP.

So how do we correct it? The way to make it follow LSP is that we can create separate interfaces for vehicles with engines & without engines, in this way, we will have a clear idea about different vehicles.

from abc import ABC, abstractmethod

class TransporationDevice(ABC):
    @abstractmethod
    def start(self):
        """Steps to start the vehicle"""

class TransporationDeviceWithoutEngine(ABC):
    pass

class TransporationDeviceWithEngine(ABC):        
    @abstractmethod
    def start_engine(self):
        """Process to start the engine"""

class Car(TransporationDeviceWithEngine):

        def start(self):
            # First start engine
            self.start_engine()
            # Do other steps here

        def start_engine(self):
           print("Starting Engine...")

class Bike(TransporationDeviceWithEngine):
        def start(self):
            # First start the engine
            self.start_engine()
            # Do other steps here

        def start_engine(self):
           print("Starting Engine...")


class Cycle(TransporationDeviceWithoutEngine):
        def start(self):
            print("Hit pedals....")

Now we have created a hierarchy of interfaces for different types of transportation devices. Now our client code can accept the TransporationDevice type object without breaking any code.

Here is the checklist to see if your code breaks LSP:

No new exceptions should be thrown in a derived class: If your base class threw ValueError then your sub-classes were only allowed to throw exceptions of type ValueError or any exceptions derived from ValueError. Throwing NotImpelementedError is a violation of LSP. We have seen this in one of the examples.

Pre-conditions cannot be strengthened: Assume your base class works with a member int. Now your sub-type requires that int be positive. This is strengthened pre-conditions, and now any code that worked perfectly fine before with negative ints is broken.

Post-conditions cannot be weakened: Assume your base class returns a list datatype as a response, you overrode that method and decided to return only a single value instead of a list as your list contains a single value only. You have weakened the post-conditions of that method.

Invariants must be preserved: The most difficult and painful constraint to fulfill. Invariants are sometimes hidden in the base class and the only way to reveal them is to read the code of the base class. An invariant is like a rule or an assumption that can be used to dictate the logic of your program. Basically, you have to be sure when you override a method anything unchangeable must remain unchanged after your overridden method is executed. Read more about it here

History constraint (the "history rule"): Objects are regarded as being modifiable only through their methods - Wikipedia. It basically means you should not modify the state of the base class which is supposed to be immutable or only changed by its own private methods.

This is Liskov's substitution principle in a nutshell for you. Some of these constraints are hard to achieve but it's it is useful as good advice on how to properly inherit in your code.

The LSP is more concerned about the behavior of the code than the structure of the code. You can follow these two thumb rules to check you are not completely breaking LSP.

  • Ask yourself if this child is really a type of the parent class or child is-a parent.
  • Will the child class will break the code if replaced by the parent class anywhere?

Since it is hard to completely follow LSP without breaking any of its constraints people tend to favor composition over inheritance more but still, LSP has its own merits & we should follow it to do inheritance properly. We will discuss this Inheritance Vs Composition in some other article.

Hope you have learned something new today. Feel free to write your feedback in the comments.