Why this matters
The ISP is about avoiding unnecessary coupling through over-broad interfaces. In statically typed languages, depending on a module that contains things you don't use means your code must be recompiled whenever any of those unused things change. In dynamically typed languages, it means your deployment must happen whenever an unrelated part of the dependency is modified.
Martin extends ISP to the architectural level: depending on a component means depending on everything that component contains. If a component is split by ISP — each interface segregated so that clients only depend on what they call — then a bug in an unused feature of that component cannot force a redeployment of your service. The fat interface is ISP's central enemy: if some implementors must leave methods empty or raise errors for methods they don't support, the interface is doing too much and carrying baggage that poisons unrelated consumers.
The problem
A fat Worker interface forces Robot to implement eat() and sleep() with empty stubs — meaningless coupling to behavior it will never use.
Bad
from abc import ABC, abstractmethod
class Worker(ABC):
@abstractmethod
def work(self) -> None: ...
@abstractmethod
def eat(self) -> None: ... # meaningless for robots
@abstractmethod
def sleep(self) -> None: ... # meaningless for robots
class Robot(Worker):
def work(self) -> None: print("Processing...")
def eat(self) -> None: pass # forced stub
def sleep(self) -> None: pass # forced stub
interface Worker {
work(): void;
eat(): void; // robots don't eat
sleep(): void; // robots don't sleep
}
class Robot implements Worker {
work(): void { console.log("Processing..."); }
eat(): void { /* forced empty stub */ }
sleep(): void { /* forced empty stub */ }
}
The solution
Three focused interfaces let each class depend only on the methods it actually uses. Robot implements Workable only — no empty stubs, no unnecessary coupling.
Good
from abc import ABC, abstractmethod
class Workable(ABC):
@abstractmethod
def work(self) -> None: ...
class Eatable(ABC):
@abstractmethod
def eat(self) -> None: ...
class Sleepable(ABC):
@abstractmethod
def sleep(self) -> None: ...
class HumanWorker(Workable, Eatable, Sleepable):
def work(self) -> None: print("Working...")
def eat(self) -> None: print("Eating...")
def sleep(self) -> None: print("Sleeping...")
class Robot(Workable): # depends only on what it uses
def work(self) -> None: print("Processing...")
interface Workable { work(): void; }
interface Eatable { eat(): void; }
interface Sleepable { sleep(): void; }
class HumanWorker implements Workable, Eatable, Sleepable {
work(): void { console.log("Working..."); }
eat(): void { console.log("Eating..."); }
sleep(): void { console.log("Sleeping..."); }
}
class Robot implements Workable { // depends only on what it uses
work(): void { console.log("Processing..."); }
}
Key takeaway
An interface with methods that some implementors must leave empty is a design smell — it forces unnecessary coupling. Split it until every implementor genuinely needs every method it must provide.