I — Interface Segregation Principle
Introduction
Imagine you sign up for a gym membership. The contract says you must pay for tennis courts, a swimming pool, a sauna, personal training sessions, and rock climbing — even if you only want the treadmill. You are forced to pay for services you will never use.
In software, the same thing happens with interfaces. A "fat" interface forces every implementing class to deal with methods it does not need. Classes end up with dummy implementations, empty method bodies, or NotImplementedError stubs — all signs that the interface is doing too much.
The Interface Segregation Principle (ISP) is the "I" in SOLID. It states:
No client should be forced to depend on methods it does not use.
In practical terms: prefer many small, focused interfaces over one large general-purpose interface. Each interface should represent a single capability or role. Implementing classes should only sign up for the contracts they can genuinely fulfill.
ISP directly addresses a major source of LSP violations. In the previous tutorial, we saw ReadOnlyArchive throwing PermissionError on save() because it was forced to implement a FileStorage interface that included write operations. ISP says: split FileStorage into Readable and Writable so ReadOnlyArchive only needs to implement what it actually supports.

The Problem: Fat Interfaces
A fat interface is one that bundles too many unrelated responsibilities into a single contract. The symptoms are unmistakable:
-
Empty or dummy method implementations — Classes implement methods with empty bodies, return
null/None, or throw exceptions because they cannot support that operation. -
God interfaces — An interface keeps growing as new features are added, eventually reaching 10, 15, or 20 methods. Every new implementation must deal with all of them.
-
Clients importing methods they never call — A function accepts an interface and uses 2 out of 12 methods. It is coupled to 10 methods it does not care about.
-
Ripple effect changes — Adding a new method to the fat interface forces every implementing class to change, even those that have nothing to do with the new method.
Let us see a concrete example.
# VIOLATION: One fat interface forces all implementations
# to deal with methods they cannot support
from abc import ABC, abstractmethod
class MultiFunctionDevice(ABC):
"""A device that can print, scan, fax, and staple."""
@abstractmethod
def print_document(self, document: str) -> None:
pass
@abstractmethod
def scan_document(self) -> str:
pass
@abstractmethod
def fax_document(self, document: str, number: str) -> None:
pass
@abstractmethod
def staple_pages(self, pages: list[str]) -> None:
pass
class OfficeAllInOne(MultiFunctionDevice):
"""Full-featured office machine — implements everything."""
def print_document(self, document: str) -> None:
print(f"Printing: {document}")
def scan_document(self) -> str:
return "scanned_content"
def fax_document(self, document: str, number: str) -> None:
print(f"Faxing {document} to {number}")
def staple_pages(self, pages: list[str]) -> None:
print(f"Stapling {len(pages)} pages")
class BasicPrinter(MultiFunctionDevice):
"""A simple printer — can only print.
Forced to implement scan, fax, and staple."""
def print_document(self, document: str) -> None:
print(f"Printing: {document}")
def scan_document(self) -> str:
raise NotImplementedError("BasicPrinter cannot scan")
def fax_document(self, document: str, number: str) -> None:
raise NotImplementedError("BasicPrinter cannot fax")
def staple_pages(self, pages: list[str]) -> None:
raise NotImplementedError("BasicPrinter cannot staple")
class OldFaxMachine(MultiFunctionDevice):
"""A fax machine — can only fax.
Forced to implement print, scan, and staple."""
def print_document(self, document: str) -> None:
raise NotImplementedError("FaxMachine cannot print")
def scan_document(self) -> str:
raise NotImplementedError("FaxMachine cannot scan")
def fax_document(self, document: str, number: str) -> None:
print(f"Faxing {document} to {number}")
def staple_pages(self, pages: list[str]) -> None:
raise NotImplementedError("FaxMachine cannot staple")// VIOLATION: Fat interface forces dummy implementations
interface MultiFunctionDevice {
void printDocument(String document);
String scanDocument();
void faxDocument(String document, String number);
void staplePages(List<String> pages);
}
class OfficeAllInOne implements MultiFunctionDevice {
@Override
public void printDocument(String document) {
System.out.println("Printing: " + document);
}
@Override
public String scanDocument() {
return "scanned_content";
}
@Override
public void faxDocument(String document,
String number) {
System.out.println("Faxing " + document
+ " to " + number);
}
@Override
public void staplePages(List<String> pages) {
System.out.println("Stapling " + pages.size()
+ " pages");
}
}
class BasicPrinter implements MultiFunctionDevice {
@Override
public void printDocument(String document) {
System.out.println("Printing: " + document);
}
@Override
public String scanDocument() {
// Cannot scan — forced to implement anyway
throw new UnsupportedOperationException(
"BasicPrinter cannot scan");
}
@Override
public void faxDocument(String document,
String number) {
throw new UnsupportedOperationException(
"BasicPrinter cannot fax");
}
@Override
public void staplePages(List<String> pages) {
throw new UnsupportedOperationException(
"BasicPrinter cannot staple");
}
}Notice the pattern:
OfficeAllInOneimplements all 4 methods — fine.BasicPrintercan only print but is forced to implementscan,fax, andstaple— all throwNotImplementedError.OldFaxMachinecan only fax but is forced to implement 3 methods it cannot support.
Every NotImplementedError stub is both an LSP violation (the subtype cannot substitute for the interface) and an ISP violation (the interface forces methods the implementation cannot support). These two violations frequently appear together because they share the same root cause — the interface is too broad.
Applying ISP — Segregating Interfaces
The fix is to split the fat MultiFunctionDevice into small, role-based interfaces. Each interface represents one capability.
# FIX: Segregated interfaces — each represents one capability
from abc import ABC, abstractmethod
class Printable(ABC):
@abstractmethod
def print_document(self, document: str) -> None:
pass
class Scannable(ABC):
@abstractmethod
def scan_document(self) -> str:
pass
class Faxable(ABC):
@abstractmethod
def fax_document(self, document: str,
number: str) -> None:
pass
class Stapleable(ABC):
@abstractmethod
def staple_pages(self, pages: list[str]) -> None:
pass
class OfficeAllInOne(Printable, Scannable,
Faxable, Stapleable):
"""Full-featured device — opts into ALL interfaces."""
def print_document(self, document: str) -> None:
print(f"Printing: {document}")
def scan_document(self) -> str:
return "scanned_content"
def fax_document(self, document: str,
number: str) -> None:
print(f"Faxing {document} to {number}")
def staple_pages(self, pages: list[str]) -> None:
print(f"Stapling {len(pages)} pages")
class BasicPrinter(Printable):
"""Only implements Printable — no dummy methods."""
def print_document(self, document: str) -> None:
print(f"Printing: {document}")
class OldFaxMachine(Faxable):
"""Only implements Faxable — no dummy methods."""
def fax_document(self, document: str,
number: str) -> None:
print(f"Faxing {document} to {number}")
# Client code depends only on the interface it needs
def print_report(printer: Printable) -> None:
printer.print_document("Q4 Financial Report")
def send_fax(fax: Faxable) -> None:
fax.fax_document("Contract", "+1-555-0100")
# All valid — no surprises, no NotImplementedError
print_report(OfficeAllInOne())
print_report(BasicPrinter())
send_fax(OfficeAllInOne())
send_fax(OldFaxMachine())// FIX: Segregated interfaces — each represents one capability
interface Printable {
void printDocument(String document);
}
interface Scannable {
String scanDocument();
}
interface Faxable {
void faxDocument(String document, String number);
}
interface Stapleable {
void staplePages(List<String> pages);
}
class OfficeAllInOne implements Printable, Scannable,
Faxable, Stapleable {
@Override
public void printDocument(String document) {
System.out.println("Printing: " + document);
}
@Override
public String scanDocument() {
return "scanned_content";
}
@Override
public void faxDocument(String document,
String number) {
System.out.println("Faxing " + document
+ " to " + number);
}
@Override
public void staplePages(List<String> pages) {
System.out.println("Stapling " + pages.size()
+ " pages");
}
}
class BasicPrinter implements Printable {
@Override
public void printDocument(String document) {
System.out.println("Printing: " + document);
}
}
class OldFaxMachine implements Faxable {
@Override
public void faxDocument(String document,
String number) {
System.out.println("Faxing " + document
+ " to " + number);
}
}
public class Main {
static void printReport(Printable printer) {
printer.printDocument("Q4 Financial Report");
}
static void sendFax(Faxable fax) {
fax.faxDocument("Contract", "+1-555-0100");
}
public static void main(String[] args) {
printReport(new OfficeAllInOne());
printReport(new BasicPrinter());
sendFax(new OfficeAllInOne());
sendFax(new OldFaxMachine());
}
}What changed:
- Four focused interfaces:
Printable,Scannable,Faxable,Stapleable— each with exactly one method. OfficeAllInOneopts into all four — because it genuinely supports all four.BasicPrinteronly implementsPrintable— no dummy methods, no exceptions.OldFaxMachineonly implementsFaxable— clean and honest.- Client functions declare the minimum interface they need:
print_report()requires onlyPrintable, not the entire device interface.
The result: zero NotImplementedError stubs, zero LSP violations, and every class only implements what it can genuinely do.
Fixing the FileStorage Violation from LSP
In the LSP tutorial, we saw ReadOnlyArchive violating LSP because it was forced to implement save() and delete() from a FileStorage interface. ISP gives us the fix:
# FIX: Split FileStorage into read and write interfaces
from abc import ABC, abstractmethod
class Readable(ABC):
@abstractmethod
def load(self, filename: str) -> bytes:
pass
class Writable(ABC):
@abstractmethod
def save(self, filename: str, content: bytes) -> None:
pass
@abstractmethod
def delete(self, filename: str) -> None:
pass
class LocalDiskStorage(Readable, Writable):
"""Full read-write storage."""
def __init__(self):
self._files: dict[str, bytes] = {}
def save(self, filename: str, content: bytes) -> None:
self._files[filename] = content
def load(self, filename: str) -> bytes:
return self._files[filename]
def delete(self, filename: str) -> None:
del self._files[filename]
class ReadOnlyArchive(Readable):
"""Read-only archive — only implements Readable.
No need for dummy save() or delete()."""
def __init__(self, archived: dict[str, bytes]):
self._archived = archived
def load(self, filename: str) -> bytes:
return self._archived[filename]
def backup_documents(storage: Writable,
docs: dict[str, bytes]) -> None:
"""Requires Writable — will never receive ReadOnlyArchive."""
for name, content in docs.items():
storage.save(name, content)
def search_archive(storage: Readable,
filename: str) -> bytes:
"""Requires Readable — works with both."""
return storage.load(filename)
# Type safety prevents the bug entirely
local = LocalDiskStorage()
backup_documents(local, {"report.pdf": b"data"}) # Works
archive = ReadOnlyArchive({"old.pdf": b"archived"})
search_archive(archive, "old.pdf") # Works
# backup_documents(archive, {...}) # Type error! archive is
# not Writable — the bug is caught at design timeNow backup_documents() declares it needs Writable, and ReadOnlyArchive is not Writable — so the bug is impossible. ISP and LSP work together: ISP ensures interfaces are narrow enough that every implementation can honestly fulfill them, which prevents LSP violations.
Visualization

How to Detect ISP Violations
Signal 1: NotImplementedError or Empty Method Bodies
The strongest signal. If any implementing class has methods that throw exceptions, return dummy values, or have empty bodies — the interface is too broad.
Signal 2: Large Interfaces That Keep Growing
An interface with 8+ methods that gets a new method added every quarter is almost certainly a fat interface. Count the methods. If different implementations only use subsets of them, it needs splitting.
Signal 3: Parameters That Accept a Large Type But Use Only a Fraction
A function takes a MultiFunctionDevice but only calls print(). It is coupled to scan(), fax(), and staple() for no reason. If someone passes an OldFaxMachine, the function will crash.
Signal 4: Unrelated Method Groups
Look at the methods in the interface — do they fall into natural clusters? print() and scan() relate to document I/O. staple() relates to physical paper handling. fax() relates to telecommunications. These clusters suggest separate interfaces.
Common Mistakes
Mistake 1: Going Too Far — One Method Per Interface, Always
The trap: "ISP says small interfaces, so I will make every single method its own interface." You end up with 20 micro-interfaces, and classes implement 15 of them.
The fix: Group methods that are cohesively related and always used together. If every implementation that supports save() also supports delete(), they belong in one Writable interface. ISP says no client should be forced to depend on methods it does not use — not "every method must be its own interface."
Mistake 2: Using Default/Empty Implementations Instead of Splitting
The trap: "I will add default empty implementations so subclasses do not have to override them." This hides the problem — the class still conceptually depends on methods it does not support.
The fix: Default implementations are valid only when the default behavior makes sense for all implementers. An empty default for fax() does not make sense for a printer — it makes the fat interface invisible but still present.
Mistake 3: Ignoring ISP Because "We Only Have One Implementation"
The trap: "We have one OfficeAllInOne class, so a big interface is fine."
The fix: ISP is about future extensibility. The moment you add a second implementation that cannot support all methods, the fat interface becomes a liability. Segregate early when natural clusters are visible.
ISP in Real-World Frameworks
ISP is not academic theory — it is how well-designed frameworks are built:
-
Java Collections:
Iterable,Collection,List,Set,Queue,Deque— a hierarchy of increasingly specific interfaces. A method that just needs iteration acceptsIterable, notList. A method that needs indexed access acceptsList. -
Python Protocols (typing): Python uses structural subtyping. You can define
Protocolclasses likeSized(has__len__),Iterable(has__iter__),Hashable(has__hash__). Functions declare the minimum protocol they need. -
Repository Pattern in Backend APIs: Instead of one
Repositoryinterface withfind,save,delete,bulkInsert,aggregate, you can split intoReadRepositoryandWriteRepository. Query handlers only depend onReadRepository.
ISP also connects directly to the Strategy Pattern (S10) and Observer Pattern (S10). When you define a PaymentStrategy interface with only process_payment(), that is ISP in action — the interface contains exactly the method the context needs, nothing more.
Key Takeaways
- The Interface Segregation Principle says no client should be forced to depend on methods it does not use — prefer small, focused interfaces over fat ones
- Fat interfaces force implementations into dummy methods (
NotImplementedError, empty bodies), which cause LSP violations - Fix by splitting the fat interface into role-based interfaces (e.g.,
Printable,Scannable,Faxable) so each class implements only what it can genuinely do - Group methods that are cohesively related — ISP does not mean one method per interface. Methods that are always used together can stay in one interface
- Client functions should declare the minimum interface they need, reducing coupling and preventing runtime surprises
- ISP resolves the
ReadOnlyArchiveviolation from the LSP tutorial by splittingFileStorageintoReadableandWritable - Next up: the Dependency Inversion Principle (DIP) — the final SOLID principle, which tells you what your interfaces should depend on