Search code examples
pythongenericspython-typing

Type describing a generic protocol with constraints


Given the following python code:

from typing import Protocol, TypeVar


class A: pass
class B(A): pass
class C(A): pass


T = TypeVar("T", B, C, contravariant=True)


class X(Protocol[T]):
    def f(self, t: T) -> None: ...


class XTImpl(X[T]):
    def f(self, t: T) -> None: pass


class XBImpl(X[B]):
    def f(self, t: B) -> None: pass

How can I type a variable that accepts only a XTimpl instance and not XBImpl instances ?

My true use case is that I have functions

def fB(x: type[X[B]]): ...

def fC(x: type[X[C]]): ...


def fT(x):
    fB(x)
    fC(x)

And I would like to type argument x in fT so that only XTImpl is accepted. fB is expected to accept both XTImpl and XBImpl.

I would like something like just X for instance (for which I get error "Expected type arguments for generic class").

If we changed T = TypeVar("T", B, C, contravariant=True) into T = TypeVar("T", A, contravariant=True) then we could just use X[A]. But in my true use case, B and C are not direct children of a common ancestor.


Solution

  • I can see why this caused you difficulty, the contravariant condition on T proved troublesome. The solution I have below is essentially a placeholder for an intersection type in fT. Since T is limited to only two values, we can use overloads to describe each available option:

    
    @overload
    def fT(x: type[XTImpl[B]]):
        ...
    
    @overload
    def fT(x: type[XTImpl[C]]):
        ...
    
    def fT(x: type[XTImpl[Any]]):
        fB(x)
        fC(x)
    

    The interior of the function fT has a slightly broader type than it should (note the use of Any here), but by overloading at least the exterior interior of fT should now be correct. Hope this helps!