Hello! In our last lesson, we explored the Liskov Substitution Principle (LSP), which ensures that subtypes can be used interchangeably with their base types by fulfilling a "behavioral contract." A key takeaway was that fixing LSP violations often involves creating more granular abstractions. For instance, instead of forcing a Penguin to implement a fly method from a generic Bird interface, we created a more specific Flyable interface.
This lesson builds directly on that idea by introducing the Interface Segregation Principle (ISP), the "I" in SOLID. We will focus on the importance of designing fine-grained, client-specific interfaces. You'll learn why large, general-purpose interfaces can be harmful to maintainability and how to break them down into smaller, more focused ones in both Python and Rust. This principle is crucial for reducing coupling and ensuring that components are not burdened by dependencies they don't need.
The Problem with "Fat" Interfaces
The Interface Segregation Principle is formally stated as:
"Clients should not be forced to depend on methods they do not use."
An interface that bundles together many methods serving different, potentially unrelated, purposes is often called a "fat" or "polluted" interface. When a class implements such an interface, it is forced to provide implementations for all its methods, even those that are irrelevant to the class's purpose.
This often leads to code with empty method bodies, methods that raise NotImplementedError, or methods that simply pass. This isn't just messy; it's a design smell that indicates a violation of ISP. It creates unnecessary coupling and can mislead clients of the class about its actual capabilities.
The diagram below provides a classic illustration of this problem and its solution.

This principle encourages us to think from the client's perspective. What methods does this particular client actually need? The answer should define the interface.
Applying ISP in Python: Inheritance vs. Composition
Let's explore how this plays out in a practical Python example. We'll examine a payment processing system where a single, large interface causes problems and then look at two ways to fix it. The first uses interface inheritance, and the second, often preferable, uses composition.
Uncle Bob’s SOLID Principles Made Easy 🍀 - In Python!
This video from ArjanCodes provides an excellent demonstration of an ISP violation and two different refactoring strategies.
The Problem (09:45 - 11:09): Watch this segment to understand the initial setup. A PaymentProcessor abstract class includes a method for SMS authentication (auth_sms). Notice how this forces the CreditPaymentProcessor subclass, which doesn't support this feature, to implement a method it doesn't need. Solution 1: Interface Inheritance (11:09 - 12:14): Here, the presenter refactors the code by creating a more specific abstract class, PaymentProcessorSMS, that inherits from the base PaymentProcessor and adds the authentication method. This segregates the interface so that only relevant subclasses need to implement SMS authentication. Solution 2: Composition (12:14 - 15:20): This segment demonstrates a more flexible alternative. Instead of a deeper inheritance hierarchy, a separate SMSAuthorizer class is created. Payment processors that require this functionality are composed with an SMSAuthorizer instance. Pay close attention to how this decouples the authentication logic from the payment processing logic entirely.
The video clearly shows the trade-offs. While creating a more specific abstract class (PaymentProcessorSMS) solves the immediate ISP violation, it can lead to more complex and rigid inheritance trees. The composition-based approach is often cleaner and more aligned with the Single Responsibility Principle. The SMSAuthorizer has one job, and payment processors can use it if they need it, without being forced into a specific inheritance structure.
An Example from Your Domain: Computer Vision Models
The need for interface segregation is very common in ML and computer vision pipelines, where you might work with various models that have different capabilities. Let's look at an example that should be quite familiar.
SOLID Principles Applied to Data Science - Level Up Coding
This article from Level Up Coding provides a relevant example of ISP in a data science context.
First, read the definition of ISP and its explanation in the context of Python's duck typing and Abstract Base Classes (ABCs), from the beginning of the section. Next, read the use case example. Here, a base ClassifierModel class forces all subclasses to implement predict_proba. This is a problem for models like LinearSVC which don't naturally produce probabilities. Notice how the solution is to segregate the interface into a base ClassifierModel (with just predict) and an extended ClassifierModelWithProba.
This example is a direct application of ISP to a scenario you might encounter when designing a computer vision system. You could have a pipeline that processes predictions from different types of models:
- Some models might only produce class labels (e.g., a simple classifier).
- Others might produce class labels and bounding boxes (e.g., an object detector).
- Yet others might produce pixel-level segmentation masks.
Instead of creating one giant Model interface with predict_class(), predict_bounding_box(), and predict_segmentation(), ISP guides you to create separate, fine-grained interfaces: IClassPredictor, IBoundingBoxPredictor, and ISegmentationPredictor. A specific model class like YOLOv8 might implement both IClassPredictor and IBoundingBoxPredictor, while a UNet model would implement ISegmentationPredictor. This allows client code to depend only on the capabilities it requires.
Applying ISP in Rust with Traits
In Rust, traits are the natural tool for defining interfaces. ISP translates directly to designing small, focused traits rather than large, monolithic ones.
Let's quickly review how traits work and then see how they enable powerful applications of ISP.
This video from Let's Get Rusty is a great primer on traits. We'll focus on the parts most relevant to interface design.
Defining and Implementing Traits (00:53 - 02:54): Watch this section to see how a Summary trait is defined and then implemented for different structs (NewsArticle, Tweet). This is the fundamental pattern for defining shared behavior. Traits as Parameters (04:59 - 09:18): This is the crucial part for ISP. Pay attention to the impl Trait syntax. When a function's parameter is item: &impl Summary, it means the function's client only needs to provide a type that implements the Summary trait. The function is completely decoupled from any other methods the type might have, which is the essence of ISP.
The Animal example from the beginning of the lesson would be modeled in Rust with traits like Animal, SwimAbility, and FlyAbility. A Cat struct would implement Animal, while a Duck struct would implement Animal and SwimAbility.
A function that only needs to make an animal walk would have a signature like this:
fn make_it_walk(animal: &impl Animal) {
animal.walk();
}
This function depends only on the Animal trait, not on SwimAbility or FlyAbility, even if the object passed to it (like a Duck) implements them. This compile-time enforcement of minimal dependencies is one of Rust's great strengths.
Conditional Trait Implementation: ISP on Steroids
Rust's trait system allows for an even more powerful expression of ISP: conditionally implementing methods. You can define methods that only exist for a struct if its generic type parameter satisfies certain trait bounds.
This final segment from the same video shows a more advanced but powerful feature.
Watch the section on conditionally implementing methods. Notice how the Pair<T> struct has a new function that is always available, but the compare_display method is only available on a Pair<T> if the generic type T implements the Display and PartialOrd traits.
This is a profound application of ISP. The interface of the Pair<T> struct is literally segregated at compile time. The compiler will not allow you to call compare_display on a Pair<MyStruct> if MyStruct doesn't implement Display and PartialOrd. This prevents you from depending on an interface that doesn't exist for your specific use case, enforcing the principle with maximum safety.
Conclusion
The Interface Segregation Principle is a powerful tool for reducing coupling and improving the clarity and maintainability of your architecture. By creating small, role-specific interfaces, you ensure that classes and their clients are not burdened with dependencies they don't use.
Key Takeaways:
- ISP Defined: Clients should not be forced to depend on methods they don't use. Avoid "fat" interfaces.
- Signs of Violation: Classes with empty,
pass, orNotImplementedErrormethods are strong indicators that the interface they implement is too broad. - Python Strategies:
- Decompose large Abstract Base Classes (or Protocols) into smaller, more focused ones.
- Favor composition over interface inheritance to add optional behaviors, as this is often more flexible and better aligns with the Single Responsibility Principle.
- Rust Strategies:
- Design many small, focused traits instead of a few large ones (e.g.,
SwimAbility,FlyAbility). - Use
impl Traitin function signatures to depend only on the behavior you need. - Leverage conditional implementations to make parts of an interface available only when certain conditions are met.
- Design many small, focused traits instead of a few large ones (e.g.,
In this module, we've covered the first four SOLID principles, which guide the design of our components and their interfaces. In the final lesson of this module, we'll explore the Dependency Inversion Principle (DIP). It will teach us how to use these well-designed abstract interfaces to decouple high-level business logic from low-level implementation details, a cornerstone of flexible and maintainable software architecture.