Skip to content

Interface Segregation Principle

What Problem Does It Solve?

The Interface Segregation Principle (ISP) solves the problem of "fat interfaces" that force implementors to provide methods they don't need. When a single interface tries to cover every possible use case, classes implementing it end up with empty method bodies, NotImplementedError stubs, or confusing behavior for unused methods.

Think of a restaurant menu organized into sections (Appetizers, Main Course, Desserts, Drinks). A customer who only wants a drink doesn't need to look through the entire menu or pay for a full course. A well-segmented interface works the same way: consumers depend only on the methods they actually use, not on everything the system can do.

How to Identify When to Use It

  • A class implements an interface with methods it doesn't need (empty bodies or raise NotImplementedError)
  • You see fat interfaces with 10+ methods
  • Changing one method signature on an interface forces updates across unrelated classes
  • Clients depend on methods they never call (passing unused dependencies)
  • The interface contains multiple distinct "categories" of behavior

Questions to ask yourself:

  • Do all methods on this interface make sense together? Or are there two or three "groups"?
  • Would any implementor ever want to implement only half of these methods?
  • If I split this interface, would the caller and implementor be happier?

Red flags: Interfaces named IEverythingService, Manager, or AllInOne. Classes with pass methods. Comments like "not supported" next to an implementation. Wide interfaces that are implemented by a single class (might still be okay, but worth examining).

How to Apply It

  1. Identify different "roles" or "clients" that use the interface
  2. Extract each role's specific needs into a separate, focused interface
  3. Have concrete classes implement only the interfaces they need
  4. Keep interfaces small — typically 1–5 methods
python
from abc import ABC, abstractmethod

# ❌ Before — Fat interface forces all workers to implement everything
class WorkerInterface(ABC):
    @abstractmethod
    def work(self) -> None:
        pass

    @abstractmethod
    def eat(self) -> None:
        pass

    @abstractmethod
    def sleep(self) -> None:
        pass

class HumanWorker(WorkerInterface):
    def work(self) -> None:
        print("Human working")

    def eat(self) -> None:
        print("Human eating")

    def sleep(self) -> None:
        print("Human sleeping")

class RobotWorker(WorkerInterface):
    def work(self) -> None:
        print("Robot working (never stops)")

    def eat(self) -> None:
        raise NotImplementedError("Robots don't eat")  # ❌

    def sleep(self) -> None:
        pass  # ❌ Lying — robot doesn't sleep but caller thinks it does

# ✅ After — Segregated interfaces
class Workable(ABC):
    @abstractmethod
    def work(self) -> None:
        pass

class Eatable(ABC):
    @abstractmethod
    def eat(self) -> None:
        pass

class Sleepable(ABC):
    @abstractmethod
    def sleep(self) -> None:
        pass

class HumanWorker(Workable, Eatable, Sleepable):
    def work(self) -> None:
        print("Human working")

    def eat(self) -> None:
        print("Human eating")

    def sleep(self) -> None:
        print("Human sleeping")

class RobotWorker(Workable):
    def work(self) -> None:
        print("Robot working (never stops)")
    # No eat() or sleep() — not needed, no stubs

Real-World Example

A notification system where different notification types have different capabilities (some can schedule, some can include attachments, some can only send text).

python
from abc import ABC, abstractmethod

# Segregated interfaces — each covers one capability
class TextSender(ABC):
    @abstractmethod
    def send_text(self, to: str, message: str) -> None:
        pass

class Schedulable(ABC):
    @abstractmethod
    def schedule(self, to: str, message: str, at: str) -> None:
        pass

class Attachable(ABC):
    @abstractmethod
    def send_with_attachment(self, to: str, message: str, file_path: str) -> None:
        pass

# Each notification type implements only what it supports
class EmailNotifier(TextSender, Schedulable, Attachable):
    def send_text(self, to: str, message: str) -> None:
        print(f"Emailing {to}: {message}")

    def schedule(self, to: str, message: str, at: str) -> None:
        print(f"Scheduling email to {to} at {at}: {message}")

    def send_with_attachment(self, to: str, message: str, file_path: str) -> None:
        print(f"Emailing {to} with attachment {file_path}: {message}")

class SMSNotifier(TextSender, Schedulable):
    def send_text(self, to: str, message: str) -> None:
        print(f"Texting {to}: {message}")

    def schedule(self, to: str, message: str, at: str) -> None:
        print(f"Scheduling SMS to {to} at {at}: {message}")
    # No send_with_attachment — SMS doesn't support it

class PushNotifier(TextSender):
    def send_text(self, to: str, message: str) -> None:
        print(f"Push notification to {to}: {message}")
    # No scheduling, no attachments — just send

# Callers only depend on what they need
class AlertService:
    def __init__(self, sender: TextSender):
        self._sender = sender  # only needs text sending

    def send_alert(self, user_id: str, alert: str) -> None:
        self._sender.send_text(user_id, alert)

class ScheduledReportService:
    def __init__(self, scheduler: Schedulable):
        self._scheduler = scheduler  # only needs scheduling

    def send_report(self, email: str, when: str) -> None:
        self._scheduler.schedule(email, "Monthly report ready", when)

SMSNotifier doesn't have a useless send_with_attachment stub. PushNotifier doesn't need a schedule stub. Each class implements only what it genuinely supports.

Common Mistakes / Pitfalls

  • Over-segmentation: An interface with one method is fine, but if you have 15 single-method interfaces, you've probably gone too far. Group related behaviors together.
  • Ignoring the client perspective: ISP is about the client's needs, not the implementor's convenience. Split interfaces when clients need different subsets of functionality.
  • Leaky abstractions in Python: Python doesn't enforce interfaces at compile time. Document the interface contract clearly even if using duck typing.
  • Not applying ISP to function parameters: A function that accepts a large interface but only uses one method forces callers to provide a large object. Accept the specific type you need.
  • Interface proliferation: Creating a new interface for every single client can lead to too many types. Find the natural groupings before splitting.

<<>> with ♥️ by S@Nchit