Keep the Promise
The Liskov Substitution Principle says: anywhere the code expects a protocol, every implementation of that protocol must work correctly. An implementation that technically fits but behaves badly — raises, does nothing, lies — breaks every function that trusted the protocol.
A protocol is a promise. fly() promises flying. A class that implements fly() by raising an exception has signed a contract it cannot keep.
A Substitute That Breaks the Deal
The Bird protocol promises fly(). Sparrows deliver. Penguins are birds too — so it feels natural to implement the protocol... with a raise, because penguins cannot fly.
Now air_show, which works for every honest bird, crashes on a penguin. The bug is not in air_show — it is in the substitute that broke the promise.
The penguin that crashes the show
air_show trusts the protocol and calls fly() on every bird. The sparrow performs; the penguin raises an exception and takes the whole show down (the try/except only softens the landing to show the message).
Output: complete Python program
from typing import Protocol
class Bird(Protocol):
def fly(self) -> str: ...
class Sparrow:
def fly(self) -> str:
return "sparrow: flying low"
# Penguin signs the contract — but cannot keep it.
class Penguin:
def fly(self) -> str:
raise RuntimeError("penguins cannot fly")
def air_show(birds: list[Bird]) -> None:
for b in birds:
print(b.fly())
try:
air_show([Sparrow(), Penguin()])
except RuntimeError as err:
print("the show crashed:", err)
Separate the Promises
The fix: promise only what is true. Split the fat promise into Flyer and Swimmer. The sparrow implements Flyer, the penguin implements Swimmer — and no class signs a contract it cannot keep.
The best part is who catches the mistake now: a type checker (e.g. mypy) reports air_show([Penguin()]) before the code ever runs, because a penguin has no fly() method — plain python3 would fail only when fly() is called.
Every class keeps its promise
air_show accepts Flyer, pool_show accepts Swimmer. Each animal appears only where its promise holds — substitution is safe by construction.
Output: complete Python program
from typing import Protocol
class Flyer(Protocol):
def fly(self) -> str: ...
class Swimmer(Protocol):
def swim(self) -> str: ...
class Sparrow:
def fly(self) -> str:
return "sparrow: flying low"
class Penguin:
def swim(self) -> str:
return "penguin: diving deep"
def air_show(flyers: list[Flyer]) -> None:
for f in flyers:
print(f.fly())
def pool_show(swimmers: list[Swimmer]) -> None:
for s in swimmers:
print(s.swim())
air_show([Sparrow()])
pool_show([Penguin()])
# air_show([Penguin()]) — a type checker (e.g. mypy) reports this
# before the code runs; plain python3 fails only when fly() is called.
Any Flyer can join
The payoff of honest protocols: a new Eagle joins the air show with no risk — anything that implements Flyer truly flies, so air_show never needs to doubt its input.
Output: complete Python program
from typing import Protocol
class Flyer(Protocol):
def fly(self) -> str: ...
class Sparrow:
def fly(self) -> str:
return "sparrow: flying low"
# Eagle is new — and it truly flies, so it substitutes safely.
class Eagle:
def fly(self) -> str:
return "eagle: soaring high"
def air_show(flyers: list[Flyer]) -> None:
for f in flyers:
print(f.fly())
air_show([Sparrow(), Eagle()])