Skip to main content

Interface Segregation Principle (ISP)

“Clients should not be forced to depend on methods they do not use.” In essence, large interfaces should be broken into smaller, role-specific interfaces so that implementing classes only need to know about the methods that matter to them.

Purpose:

  • Avoids “fat” interfaces – by splitting broad interfaces into focused ones, no class is burdened with irrelevant methods.
  • Reduces coupling and side effects: Changes to one part of an interface won’t impact classes that don’t use that part.
  • Makes implementations simpler and more flexible. A class can implement multiple small interfaces as needed, rather than one monolithic interface.

Minimal Example

Imagine a multi-function printer interface that includes printing, scanning, and faxing in one. A simple printer that only prints would still be forced to implement (or stub out) scan() and fax() methods, which it doesn’t need – violating ISP. Below, MultiFunctionDevice represents such a bloated interface and OldPrinter is needlessly forced to depend on unused methods. The solution is to segregate the interfaces: separate Printer, Scanner, and Fax interfaces so that classes implement only what they actually support.
# Big interface (violates ISP):
class MultiFunctionDevice:
    def print(self, doc): raise NotImplementedError
    def scan(self, doc): raise NotImplementedError
    def fax(self, doc):  raise NotImplementedError

class OldPrinter(MultiFunctionDevice):
    def print(self, doc):
        print("Printing:", doc)
    def scan(self, doc):
        pass  # Not supported, but must implement
    def fax(self, doc):
        pass  # Not supported, but must implement

# Interface segregation: smaller, specific interfaces
class Printer:
    def print(self, doc): raise NotImplementedError

class Scanner:
    def scan(self, doc): raise NotImplementedError

class Fax:
    def fax(self, doc): raise NotImplementedError

class SimplePrinter(Printer):
    def print(self, doc):
        print("Printing:", doc)
# SimplePrinter only implements Printer, no need for scan/fax methods.

More Realistic Example

Building on the printing example, we can have distinct interfaces and classes for each capability. For instance, PhotoPrinter implements Printer, OfficeScanner implements Scanner. A multi-function machine can be composed by combining a printer and a scanner implementation without any class having to implement methods it doesn’t use. This demonstrates how ISP enables flexible combinations of behaviors:
class PhotoPrinter(Printer):
    def print(self, doc):
        print("Printing photo:", doc)

class OfficeScanner(Scanner):
    def scan(self, doc):
        print("Scanning document:", doc)

class MultiFunctionMachine(Printer, Scanner):
    def __init__(self, printer: Printer, scanner: Scanner):
        self.printer = printer
        self.scanner = scanner
    def print(self, doc):
        # Delegate to an internal Printer
        self.printer.print(doc)
    def scan(self, doc):
        # Delegate to an internal Scanner
        self.scanner.scan(doc)

# Using the segregated interfaces:
printer = PhotoPrinter()
scanner = OfficeScanner()
mfp = MultiFunctionMachine(printer, scanner)  # compose a multi-function device

mfp.print("image.png")   # Output: Printing photo: image.png
mfp.scan("file.txt")     # Output: Scanning document: file.txt

# We could easily swap in different implementations for each interface as needed.