Card 07/ 09
isinstance Checks in the Caller as a Liskov Symptom
if isinstance(bird, Penguin) means the substitution is already broken.
A caller that checks which subtype it was given, and treats one specially, is compensating for a subtype that broke its parent's promise. The check looks like careful defensive code. It is actually the visible symptom of a Liskov violation, and this card shows how to read it that way.
The expectation is that checking is safe: if you know penguins cannot fly, skipping them seems responsible. Here is that defensive version, which runs without errors.
The type check, and the design that removes it
With the check
migrate accepts any Bird and skips penguins. It works, so the check is easy to approve.
class Bird:
def __init__(self, name):
self.name = name
def fly(self):
return f"{self.name} flies south"
class Penguin(Bird):
def fly(self):
raise NotImplementedError("penguins cannot fly")
def migrate(birds):
for bird in birds:
if isinstance(bird, Penguin):
continue
print(bird.fly())
migrate([Bird("swallow"), Penguin("penguin")])swallow flies southWithout the need
Bird no longer promises fly. Only FlyingBird does, and migrate only accepts flying birds. There is nothing to check.
class Bird:
def __init__(self, name):
self.name = name
def eat(self):
return f"{self.name} eats"
class FlyingBird(Bird):
def fly(self):
return f"{self.name} flies south"
class Sparrow(FlyingBird):
pass
class Ostrich(Bird):
def run(self):
return f"{self.name} runs at 70 km/h"
def migrate(birds: list[FlyingBird]):
for bird in birds:
print(bird.fly())
def feed(birds: list[Bird]):
for bird in birds:
print(bird.eat())
sparrow, ostrich = Sparrow("sparrow"), Ostrich("ostrich")
migrate([sparrow])
feed([sparrow, ostrich])sparrow flies south
sparrow eats
ostrich eatsThe first version has three problems hidden behind its clean output. Every caller of fly needs the same check, and only the ones you remember will get it. The check names a concrete subclass, so migrate now depends on Penguin, and adding a kiwi means editing migrate. And the parent's promise still says every bird flies, so the next developer to write a caller will believe it.
The check is not always wrong. Code at a boundary, such as a function that turns any value into JSON, often has to ask what it was given. The symptom is a check that singles out one subclass of a hierarchy you own, in code that was written for the parent. When you see isinstance(x, SomeSubclass) or instanceof in that position, look at the hierarchy before approving the check.
← → move through sections and cards