The Interface Segregation Principle (ISP) is the “I” in SOLID, and it has a simple but powerful message: “Clients should not be forced to depend on interfaces they do not use.”
In simpler terms, you shouldn’t force a class to implement a method it doesn’t need. Doing so creates “fat” interfaces that are bloated, confusing, and brittle. The solution is to break them down into smaller, more specific ones.
Let’s take the analogy of a multifunction printer. Imagine you have a basic printer that only prints. If its control software includes buttons for “Scan” and “Fax,” you’ve created a confusing user experience. Pressing “Scan” would do nothing or, worse, throw an error. This is what ISP helps us avoid in our code.
[!NOTE] In the context of ISP, a “client” is any class or module that uses an interface. The principle is about designing interfaces from the client’s perspective.
The Problem: A “Fat” User Management Interface
Imagine we’re building a user management system. We have two types of users:
AdminUser: Can view content, add new users, and remove users.RegularUser: Can only view content.
A naive first approach might be to create a single, all-encompassing interface (or Abstract Base Class in Python) for all user-related actions.
graph TD;
subgraph Problematic Design
IUserManagement[<br><br>**IUserManagement (ABC)**<br>+ view_content()<br>+ add_user()<br>+ remove_user()<br><br>]
AdminUser(AdminUser)
RegularUser(RegularUser)
IUserManagement -- implements --> AdminUser;
IUserManagement -- implements --> RegularUser;
style RegularUser fill:#f9f,stroke:#333,stroke-width:2px
end
Let’s model this “fat” interface in Python.
# A single, "fat" interface for all user actions
from abc import ABC, abstractmethod
class IUserManagement(ABC):
"""A bloated interface handling all user actions."""
@abstractmethod
def view_content(self):
pass
@abstractmethod
def add_user(self, username: str):
pass
@abstractmethod
def remove_user(self, username: str):
pass
The AdminUser can implement all these methods without a problem. But what about the RegularUser? It’s forced to implement add_user and remove_user, even though it has no use for them.
class AdminUser(IUserManagement):
"""An admin can perform all actions."""
def view_content(self):
print("Admin viewing content...")
def add_user(self, username: str):
print(f"Admin adding user: {username}...")
def remove_user(self, username: str):
print(f"Admin removing user: {username}...")
class RegularUser(IUserManagement):
"""A regular user is forced to implement methods it doesn't need."""
def view_content(self):
print("Regular user viewing content...")
def add_user(self, username: str):
# This method is useless for a RegularUser!
raise NotImplementedError("Regular users cannot add other users.")
def remove_user(self, username: str):
# This method is also useless!
raise NotImplementedError("Regular users cannot remove other users.")
This design is a clear violation of ISP. The RegularUser is polluted with methods it cannot and should not implement.
The Link to Liskov Substitution Principle (LSP)
This bad design also breaks another SOLID principle: the Liskov Substitution Principle (LSP).
[!WARNING] ISP and LSP are Interconnected When a class is forced to implement methods it doesn’t need, it often resorts to raising exceptions. This means you can’t safely substitute the subclass (
RegularUser) for its parent (IUserManagement) without risking a runtime crash, which is a direct violation of LSP.
Consider this function. It expects any object that adheres to the IUserManagement contract, but it will crash if you pass in a RegularUser.
def perform_add(user_manager: IUserManagement, new_user: str):
# This function should work for any IUserManagement type, but it won't.
print(f"Attempting to add {new_user}...")
user_manager.add_user(new_user) # <-- This will crash for RegularUser!
# ---
admin = AdminUser()
regular = RegularUser()
perform_add(admin, "new_dev") # Works fine
perform_add(regular, "hacker") # Raises NotImplementedError!
The Solution: Segregate the Interface
The solution is to break our “fat” interface into smaller, more focused, role-based interfaces.
First, let’s organize our project structure to reflect this separation.
user_system/
├── interfaces/
│ ├── __init__.py
│ ├── content_viewer.py
│ └── user_manager.py
├── roles/
│ ├── __init__.py
│ ├── admin.py
│ └── regular.py
└── main.py
Now, let’s see the refactoring from the old, bloated interface to the new, segregated ones.
--- a/before.py
+++ b/after.py
@@ -1,16 +1,20 @@
from abc import ABC, abstractmethod
-class IUserManagement(ABC):
- """A bloated interface handling all user actions."""
- @abstractmethod
- def view_content(self):
- pass
-
+class IContentViewer(ABC):
+ """Interface for entities that can view content."""
@abstractmethod
- def add_user(self, username: str):
+ def view_content(self):
pass
+class IUserManager(ABC):
+ """Interface for entities that can manage users."""
@abstractmethod
- def remove_user(self, username: str):
+ def add_user(self, username: str):
pass
+ @abstractmethod
+ def remove_user(self, username: str):
+ pass
With these new, granular interfaces, our classes can now implement only the functionality they truly need. Python’s multiple inheritance makes composing these roles elegant.
graph TD;
subgraph Clean ISP Design
IContentViewer[<br>**IContentViewer**<br>+ view_content()<br>]
IUserManager[<br>**IUserManager**<br>+ add_user()<br>+ remove_user()<br>]
AdminUser(AdminUser)
RegularUser(RegularUser)
IContentViewer -- implements --> AdminUser;
IUserManager -- implements --> AdminUser;
IContentViewer -- implements --> RegularUser;
style RegularUser fill:#cfc,stroke:#333,stroke-width:2px
end
Here is the clean implementation:
# interfaces/content_viewer.py
from abc import ABC, abstractmethod
class IContentViewer(ABC):
@abstractmethod
def view_content(self):
pass
# interfaces/user_manager.py
from abc import ABC, abstractmethod
class IUserManager(ABC):
@abstractmethod
def add_user(self, username: str):
pass
@abstractmethod
def remove_user(self, username: str):
pass
# roles/admin.py
from ..interfaces.content_viewer import IContentViewer
from ..interfaces.user_manager import IUserManager
class AdminUser(IContentViewer, IUserManager):
"""Implements only the interfaces it needs."""
def view_content(self):
print("Admin viewing content...")
def add_user(self, username: str):
print(f"Admin adding user: {username}...")
def remove_user(self, username: str):
print(f"Admin removing user: {username}...")
# roles/regular.py
from ..interfaces.content_viewer import IContentViewer
class RegularUser(IContentViewer):
"""Clean and focused: only implements what it can do."""
def view_content(self):
print("Regular user viewing content...")
Now, our RegularUser class is clean, focused, and no longer violates ISP or LSP. It only knows about view_content, as it should.
admin = AdminUser()
regular = RegularUser()
admin.view_content()
admin.add_user("new_admin")
regular.view_content()
# This would now cause a compile-time/linting error, which is much better!
# regular.add_user("another_user") # AttributeError: 'RegularUser' object has no attribute 'add_user'
Summary: Key Takeaways
A mindmap can help summarize the core ideas of the Interface Segregation Principle.
mindmap
root((Interface Segregation Principle))
Definition
:Clients should not depend on methods they don't use.
Problem: "Fat" Interfaces
Causes
::Bloated Classes
::Tight Coupling
::Poor Readability
Violates
::Liskov Substitution Principle (LSP)
::Single Responsibility Principle (SRP)
Solution: Segregation
:Break down large interfaces into smaller, role-based ones.
Benefits
::Improved Cohesion
::Reduced Coupling
::Better Maintainability
::Clearer Code
[!TIP] Best Practice: When designing interfaces, think about the roles your clients play. If a client only needs a fraction of an interface’s methods, it’s a strong signal that the interface should be split.
Test Your Knowledge
Quiz: Which of the following scenarios is the BEST candidate for applying the Interface Segregation Principle?
- A `Database` class with `connect()`, `disconnect()`, and `query()` methods.
- An `IAnimal` interface with `eat()`, `sleep()`, `walk()`, and `fly()` methods, implemented by `Bird` and `Dog` classes.
- A `MathUtils` class with static methods like `add()`, `subtract()`, and `multiply()`.
Click for the Answer
Answer: 2. The `IAnimal` interface is the best candidate. A `Dog` class would be forced to implement the `fly()` method, which it cannot do. This is a classic ISP violation. The interface should be segregated into smaller ones like `ICanWalk`, `ICanEat`, and `ICanFly`.
By embracing the Interface Segregation Principle, you write code that is more modular, easier to understand, and far more resilient to change.