Grasping the true idea behind the “Liskov Substitution Principle” seems a bit hard. The explanation that some interfaces and classes should be interchangeable never was enough for me to answer the questions “why should I care” and, consequently, “how should I implement it”. Here’s a writeup I gave for my colleagues, which they found useful, so maybe you will get something out of it as well.
What Is Liskov Substitution Principle?
So, the “L” in SOLID stands for Liskov Substitution Principle. It basically says that you should be able to substitute parent class with child classes. When you do:
class Parent: def do_something(self, arg: int): pass class Child(Parent): def do_something(self, arg: int, another_arg: int): pass
you violate it due to the fact that you won’t be able to substitute this:
p = Parent() p.do_something(123)
p = Child() p.do_something(123)
as you’ll clearly get an error from mypy static analysis.
(venv) ➜ src mypy main.py main.py:19: error: Signature of "do_something" incompatible with supertype "Parent" main.py:19: note: Superclass: main.py:19: note: def do_something(self, arg: int) -> Any main.py:19: note: Subclass: main.py:19: note: def do_something(self, arg: int, another_arg: int) -> Any Found 1 error in 1 file (checked 1 source file)
Why is Liskov Substitution Principle Important?
Well, you want to be able to introduce changes without breaking the application. That’s important if you have many dependencies and want to manage them properly. In cars you can’t change wheels if they are mounted with 5 screws and someone gives you a wheel that holds with only 1 screw. Same goes with programming. Mature libraries and frameworks put a lot of effort in designing stable interfaces so that they don’t get changed when inherited. Python C interface has one and for example the permission checking in Django Rest Framework goes like this:
To implement a custom permission, override
BasePermissionand implement either, or both, of the following methods:
.has_permission(self, request, view)
.has_object_permission(self, request, view, obj)
If you check how the already implemented permission checking classes are implemented, they all follow the above described interface:
class IsAuthenticated(BasePermission): """ Allows access only to authenticated users. """ def has_permission(self, request, view): return bool(request.user and request.user.is_authenticated) class IsAdminUser(BasePermission): """ Allows access only to admin users. """ def has_permission(self, request, view): return bool(request.user and request.user.is_staff) class IsAuthenticatedOrReadOnly(BasePermission): """ The request is authenticated as a user, or is a read-only request. """ def has_permission(self, request, view): return bool( request.method in SAFE_METHODS or request.user and request.user.is_authenticated )
If you create a class that checks permissions and has signature like:
class AllowRandomly(BasePermission): def has_permission(self): return bool(random.getrandbits(1))
you won’t be able to use it without heavily modifying the framework. Remember: interfaces must be stable and interchangeable. Whatever is implemented underneath can be volatile as much as you want. If you have 2 children 1 violating the principle, second in line with the parent, you can’t easily substitute 1 with another. This will make testing harder (as using dummy classes/mocks will be harder), making use of certain patterns like factory impossible (where you don’t know in advance which subclass you’ll receive – it’s decided in runtime)
How to Implement Liskov Substitution Principle?
Let’s get back to the example from above. How to improve that code to make it follow the principle?
- If you don’t need subclassing – don’t use it. Maybe composition instead of inheritance is the solution?
- If you need subclassing you need to design it in a way that doesn’t violate the LSP. Again: create stable interfaces.
At this point you might think I sound like a coach, who says that if you want to become rich, you need to earn money. But the basic concepts really are that simple.
Python has the concept of keyword arguments that can translate to “I don’t know what I’m going to need” if you really can’t predict the future (but think twice before using it – it’s easy to have the problems we’ve already discussed, just harder to spot and solve).
class Parent: def do_something(self, arg: int, **kwargs): pass class Child(Parent): def do_something(self, arg: int, **kwargs): pass
I strongly suggest the “Clean Architecture” by “Uncle Bob”, where he goes into more details with the LSP. He also gave a good lecture on the matter. I strongly suggest watching it all (actual start is at 12:00), but if you’re short on time, you can go from this point: