Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Workaround for lack of intersection types with Python generics?

I'm hitting an issue that would be easily solved by intersection types (currently under discussion but not yet implemented) and was wondering what the cleanest workaround is.

Current setup and problem

My current setup roughly corresponds to the following ABC hierarchy for animals. There are a number of animal 'features' (CanFly, CanSwim, etc) defined as abstract subclasses (though they could also have been defined as mixins).

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def name(self) -> str: ...  
    
class CanFly(Animal):
    @abstractmethod
    def fly(self) -> None: ...
    
class CanSwim(Animal):
    @abstractmethod
    def swim(self) -> None: ...

With this I define specific classes of animals, both abstract and concrete:

class Bird(CanFly):
    def fly(self) -> None:
        print("flap wings")
    
class Penguin(Bird, CanSwim):
    def name(self) -> str:
        return "penguin"
    def swim(self) -> None:
        print("paddle flippers")

I also define a generic class for petting specific types of animals:

from typing import Generic, TypeVar

T = TypeVar("T", bound=Animal, contravariant=True)

class Petter(Generic[T], ABC):

    @abstractmethod
    def pet(self, a: T) -> None:
        ...

However, there is no way that I know of to specify a Petter for an intersection of features: e.g. for all animals that can both fly and swim.

class CanFlyAndSwim(CanFly, CanSwim):
    pass
        
class CanFlyAndSwimPetter(Petter[CanFlyAndSwim]):

    def pet(self, a: CanFlyAndSwim):
        a.name()
        a.fly()
        a.swim()
    
        
CanFlyAndSwimPetter().pet(Penguin())  # type error, as Penguin isn't a subclass of CanFlyAndSwim

I could try to work around this by insisting that Penguin explicitly inherit from CanFlyAndSwim, but this isn't scalable to more feature combinations.

Using Protocols instead?

Another approach I tried is to use Protocols instead:

from typing import Protocol

class AnimalProtocol(Protocol):
    def name(self) -> str: ...

class FlyProtocol(AnimalProtocol, Protocol):
    def fly(self) -> None: ...

class SwimProtocol(AnimalProtocol, Protocol):
    def swim(self) -> None: ...

With these we can indeed define a useful protocol intersection. After changing the type variable T upper bound to AnimalProtocol, we can write:

class FlyAndSwimProtocol(FlyProtocol, SwimProtocol, Protocol):
    ...

class FlyAndSwimProtocolPetter(Petter[FlyAndSwimProtocol]):

    def pet(self, a: FlyAndSwimProtocol):
        a.name()
        a.fly()
        a.swim()
    
        
FlyAndSwimProtocolPetter().pet(Penguin())  # ok

However, replacing the ABCs with Protocols removes the explicit class hierarchy when defining animals, which is useful both for documentation and for checking that all the relevant methods have been implemented. We could try keeping both the ABCs and the Protocols, though that involves significant code duplication, unless there's some way to define one from the other?

Is there a clean solution to all this?

like image 322
Uri Granta Avatar asked Sep 02 '25 14:09

Uri Granta


1 Answers

First and foremost, remember that Python does duck typing extensively.

So the best solution in this case IMHO, is to:

  • Design your data using inheritance, but
  • Access attributes and methods using protocols

In other words, yeah I think you will have to mix the two approaches.

like image 123
pepoluan Avatar answered Sep 05 '25 03:09

pepoluan