Loading episodes…
0:00 0:00

SOLID's ISP: From Bloated Interfaces to Clean Python Code (A Visual Guide)

00:00
BACK TO HOME

SOLID's ISP: From Bloated Interfaces to Clean Python Code (A Visual Guide)

10xTeam November 09, 2025 8 min read

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:

  1. AdminUser: Can view content, add new users, and remove users.
  2. 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.

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?
  1. A `Database` class with `connect()`, `disconnect()`, and `query()` methods.
  2. An `IAnimal` interface with `eat()`, `sleep()`, `walk()`, and `fly()` methods, implemented by `Bird` and `Dog` classes.
  3. 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.


Join the 10xdev Community

Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.

Audio Interrupted

We lost the audio stream. Retry with shorter sentences?