Small Interfaces
The Interface Segregation Principle says: no class should be forced to implement methods it does not need. Instead of one fat interface with many methods, make several small ones — each class implements only what is true for it.
In Python an interface is written as a protocol class, and small protocols are very natural here: the language itself runs on tiny ones — any object with a __next__() method works with the built-in next(), and much of the standard library plugs into promises that small.
The Fat Interface
A Worker protocol with work() and eat() looks harmless — until a robot applies for the job. The robot works fine, but it must implement eat() too, with... something. Nonsense answers like this are the signature of a fat interface.
The robot forced to eat
The robot implements eat() only because the protocol demands it. The lunch break prints an answer that should never exist — the class was forced to lie.
Output: complete Python program
from typing import Protocol
# One fat protocol — every worker MUST also eat.
class Worker(Protocol):
def work(self) -> str: ...
def eat(self) -> str: ...
class Human:
def work(self) -> str:
return "human: working"
def eat(self) -> str:
return "human: eating lunch"
class Robot:
def work(self) -> str:
return "robot: working"
# Forced by the protocol — robots do not eat.
def eat(self) -> str:
return "robot: ERROR, cannot eat"
team: list[Worker] = [Human(), Robot()]
for w in team:
print(w.work())
for w in team:
print(w.eat())
Split the Interface
Split the fat protocol into Worker and Eater. The human implements both; the robot implements only Worker. The work shift takes workers, the lunch break takes eaters — the robot is simply never invited to lunch, and no method lies.
Each class implements only its truth
Two small protocols instead of one fat one. The robot no longer has an eat() method at all — and a type checker (e.g. mypy) keeps it out of the lunch break.
Output: complete Python program
from typing import Protocol
# Two small protocols — each class implements only its truth.
class Worker(Protocol):
def work(self) -> str: ...
class Eater(Protocol):
def eat(self) -> str: ...
class Human:
def work(self) -> str:
return "human: working"
def eat(self) -> str:
return "human: eating lunch"
class Robot:
def work(self) -> str:
return "robot: working"
def work_shift(team: list[Worker]) -> None:
for w in team:
print(w.work())
def lunch_break(guests: list[Eater]) -> None:
for e in guests:
print(e.eat())
work_shift([Human(), Robot()])
lunch_break([Human()])
Small Pieces Compose
Small protocols are not a limitation — they combine. When some function really needs both abilities, it composes them into one protocol on the spot: Worker plus Eater.
That is how small promises scale: define one-method protocols, and let whoever needs more glue them together.
Compose when you need both
WorkingEater is built from the two small protocols. The full-day shift needs both abilities, so only the human qualifies — said once, in the type system.
Output: complete Python program
from typing import Protocol
class Worker(Protocol):
def work(self) -> str: ...
class Eater(Protocol):
def eat(self) -> str: ...
# Small protocols compose into bigger ones when needed.
class WorkingEater(Worker, Eater, Protocol):
pass
class Human:
def work(self) -> str:
return "human: working"
def eat(self) -> str:
return "human: eating lunch"
def full_day(we: WorkingEater) -> None:
print(we.work())
print(we.eat())
print(we.work())
full_day(Human())