Course contents

Interface Segregation Principle

Sign in to run this lab and save your progress

Learn

Before you start

ISP is the SOLID principle that most directly punishes fat interfaces. It's also the one you'll invoke most often in real code review: "this interface is too big; split it."

The one-line version

Use many small focused interfaces instead of one big one.

Why fat interfaces hurt

Every method on an interface is a promise. A child class that says "I'm Serializable" is promising to serialize correctly. If half the children implement the method as "raise not implemented", the interface is lying about what its members can do.

Callers react in the worst possible way: they add runtime type checks.

if isinstance(obj, LegacyReport):
    ...  # skip serialization
else:
    obj.serialize(...)

Once one caller does this, others copy it. The interface stops being useful because everyone has to know the specific types anyway.

The ISP fix

Split the interface by ability. A LegacyReport doesn't inherit from Serializable at all — problem gone at compile time.

Rule of thumb: an interface should be as small as possible while still being useful to its callers.

How the SOLID letters connect

Notice how they stack on each other:

  • SRP — a class has one reason to change
  • OCP — you can add new behavior without editing old code
  • LSP — a child class doesn't surprise the parent's callers
  • ISP — a class only relies on the methods it actually uses
  • DIP (next lesson) — high-level code depends on abstractions, not specifics

They're not five random rules — they support each other. ISP is the structural fix that prevents most LSP problems. OCP is what you get when you follow the other four consistently. Mature design systems reach for them together.

About the exercise

Printers, scanners, and fax machines are the textbook ISP example because real hardware devices genuinely have different abilities. Every hardware / SaaS / ML system has this shape: a family of things, none of which does the full menu.

If your solution has raise NotImplementedError anywhere, you haven't finished. Delete the method entirely and stop inheriting the interface — that's the ISP-native answer.

Your task

I in SOLID. Uncle Bob's version: "Clients should not be forced to depend on methods they do not use."

Translation: use many small, focused interfaces instead of one big one.

The fat interface problem

A print-shop system starts with one interface:

class MultiFunctionMachine(ABC):
    @abstractmethod
    def print_doc(self, doc): ...
    @abstractmethod
    def scan(self, doc): ...
    @abstractmethod
    def fax(self, doc, number): ...
    @abstractmethod
    def copy(self, doc): ...

Now a customer buys a basic printer that only prints. What do they implement?

class BasicPrinter(MultiFunctionMachine):
    def print_doc(self, doc):
        return f"printing {doc}"
    def scan(self, doc):
        raise NotImplementedError("this printer can't scan")
    def fax(self, doc, number):
        raise NotImplementedError("this printer can't fax")
    def copy(self, doc):
        raise NotImplementedError("this printer can't copy")

Three "not implemented" methods. Any caller who thought they had a MultiFunctionMachine will crash the moment they call .scan(). This is the LSP violation ISP is built to prevent.

The ISP fix: split by ability

Give each ability its own interface:

class Printer(ABC):
    @abstractmethod
    def print_doc(self, doc): ...

class Scanner(ABC):
    @abstractmethod
    def scan(self, doc): ...

class Fax(ABC):
    @abstractmethod
    def fax(self, doc, number): ...

Now each device only implements what it can actually do:

class BasicPrinter(Printer):
    def print_doc(self, doc):
        return f"printing {doc}"

class OfficeCopier(Printer, Scanner):
    def print_doc(self, doc):
        return f"printing {doc}"
    def scan(self, doc):
        return f"scanning {doc}"

class AllInOne(Printer, Scanner, Fax):
    def print_doc(self, doc): return f"printing {doc}"
    def scan(self, doc): return f"scanning {doc}"
    def fax(self, doc, number): return f"faxing {doc} to {number}"

A caller who needs "something that can print" uses Printer — and it works with all three devices. A caller who needs "something that can scan" uses Scanner — and BasicPrinter doesn't even show up. No crashes at runtime.

How this connects to LSP

ISP is a structural fix for a common LSP problem. The fat interface FORCES child classes to lie ("yes I can scan — actually no, I'll crash"). Splitting the interface removes the reason to lie.

When to use ISP

When you see either:

  • A child class with methods that just raise "not implemented"
  • A caller that uses only 1-2 methods of a wide interface

Both mean the same fix: split the interface.

Your task

Build the ISP-clean print-shop system in main.py:

  1. Three abstract capability interfaces (each is ABC with one @abstractmethod):
    • Printerprint_doc(self, doc)
    • Scannerscan(self, doc)
    • Faxfax(self, doc, number)
  2. Three concrete devices:
    • BasicPrinter(Printer)print_doc(doc) returns "printing <doc>"
    • OfficeCopier(Printer, Scanner)print_doc(doc) returns "printing <doc>", scan(doc) returns "scanning <doc>"
    • AllInOne(Printer, Scanner, Fax) — same for printing + scanning, fax(doc, number) returns "faxing <doc> to <number>"
  3. Nothing raises NotImplementedError. Nothing pretends to implement what it can't.

Tests verify each device does only what it declared, and structurally verify that BasicPrinter is not a Scanner (proving the fat interface didn't sneak back in).

Click Submit to run the tests.

Starter files

Preview only — open the lab to edit and run.

files_dir
04-interface-segregation-starter