Skip to main content

I — Interface Segregation Principle

Editorial

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.

Interface Segregation Principle. Violation: a fat MultiFunctionDevice interface (printDocument, scanDocument, faxDocument, staplePages) forces a BasicPrinter to implement scan, fax and staple as dummies that raise NotImplementedError. Fix: split into role interfaces Printable, Scannable, Faxable, Stapleable — OfficeAllInOne implements all four, BasicPrinter only Printable, OldFaxMachine only Faxable, with no dummy methods. The same idea fixes the LSP FileStorage bug: split into Readable (load) and Writable (save, delete) so ReadOnlyArchive implements only Readable and backup_documents(storage: Writable) can never receive it. Detect fat interfaces by NotImplementedError, interfaces past ~8 methods, clients using a fraction of a type, and unrelated method clusters.

The Problem: Fat Interfaces

A fat interface is one that bundles too many unrelated responsibilities into a single contract. The symptoms are unmistakable:

  1. Empty or dummy method implementations — Classes implement methods with empty bodies, return null/None, or throw exceptions because they cannot support that operation.

  2. 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.

  3. 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.

  4. 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:

  • OfficeAllInOne implements all 4 methods — fine.
  • BasicPrinter can only print but is forced to implement scan, fax, and staple — all throw NotImplementedError.
  • OldFaxMachine can 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:

  1. Four focused interfaces: Printable, Scannable, Faxable, Stapleable — each with exactly one method.
  2. OfficeAllInOne opts into all four — because it genuinely supports all four.
  3. BasicPrinter only implements Printable — no dummy methods, no exceptions.
  4. OldFaxMachine only implements Faxable — clean and honest.
  5. Client functions declare the minimum interface they need: print_report() requires only Printable, 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 time

Now 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

A device × capability matrix. In Fat mode every device must implement all four methods, so BasicPrinter and OldFaxMachine are forced to stub the rest — click a stub and it throws NotImplementedError at runtime. Flip to Segregated role interfaces and each device implements only what it does (zero dummies); clicking a capability a device lacks shows a compile-time type error instead of a crash.

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 accepts Iterable, not List. A method that needs indexed access accepts List.

  • Python Protocols (typing): Python uses structural subtyping. You can define Protocol classes like Sized (has __len__), Iterable (has __iter__), Hashable (has __hash__). Functions declare the minimum protocol they need.

  • Repository Pattern in Backend APIs: Instead of one Repository interface with find, save, delete, bulkInsert, aggregate, you can split into ReadRepository and WriteRepository. Query handlers only depend on ReadRepository.

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 ReadOnlyArchive violation from the LSP tutorial by splitting FileStorage into Readable and Writable
  • Next up: the Dependency Inversion Principle (DIP) — the final SOLID principle, which tells you what your interfaces should depend on