Python understanding Liskov Substiution Principle

Question:

In this example, am I violating LSP? Since straight up replacing the last two lines with an instance of a subclass will give me an error(as wage isn’t initialised)?

person_1 = Employee('Brad')
person_1.print_name()
@dataclass
class Person:
    name: str

    def print_name(self):
        print(self.name)
@dataclass
class Employee(Person):
    wage: int

person_1 = Person('Brad')
person_1.print_name()

If so, then how can there ever be a non-violation of LSP when extending the constructor of a class (aside from placing optional attributes thereafter)?.

Asked By: meg hidey

||

Answers:

It depends on what you mean by the LSP.

Does it mean the strict LSP, like in Barbara Liskov’s original paper, where the behaviour of the program should be unchanged by type substitution? (and even then it’s a desired property, not an absolute requirement)

Or does it mean following the Person interface, in which case it would not be a violation, since you can’t remove functions for the Person class in the Employee class? (Well, you technically could, but it’s not a good idea to do that).

Answered By: Amaras

LSP says, that if something is true for a Person object (e.g. it has a name, the name is a string, it can print its name), it must be also true for an Employee object. In other words, every Employee is also a Person.

It does not state that an Employee object must be created the same way as a Person object. Every Employee is not only a Person. It has not only the name, but also a wage.


The second question:

If the Employee.print_name() method were redefined not to print the name, but for instance to return it as a string, that would break the principle.

Note that breaking the LSP does not require a code change, for instance if the Person’s name format were changed from e.g. "first_name last_name" to "last_name, first_name" in the Employee, and that would cause the program to give incorrect output.

Answered By: VPfB

I know it’s been answered already but i wanted to emphasize:
We need to differentiate between 2 relationships. One is the relationship between instances of Person and instances of Employee. The second one is the relationship between the 2 instances of type (The Person class itself, and the Employee class itself.)

In your case, LSP deals only with the former (everything that we can do with Person instances, we need to be able to do in the exact same way with Employee instances). It says nothing about the classes themselves.

Now, since python is very dynamic you could technically argue "Hey just wait second! There is something I can do with one and not the other!". Take a look at the following example:

# Assume we have an instance of either Person or Employee here
instance = _

# The following will work with Person instances but will raise an exception for Employee instances
instance_copy = type(instance)(instance.name)

I would say that you shouldn’t count this kind of stuff. Let’s call it an "unreasonable expectation of usage" that shouldn’t be accounted for when considering the validity of your code structure in the vast vast majority of use cases.

The most important thing to remember is this:
B inherits from A != A (the class object itself) can be substituted by B (the class itself)

Answered By: Nir Schulman