Search code examples
pythongenericsmypy

Why does mypy error if I assign only one of two generic typevars in a classmethod?


(python 3.10.6, mypy 0.990)

The following examples are all accepted by mypy:

from typing import Generic, TypeVar

T = TypeVar('T')
class Maybe(Generic[T]):
    def __init__(self, val: T):
        self._val = val
    
    @classmethod
    def empty(cls):
        return cls(None)
from typing import Generic, TypeVar

U = TypeVar('U')
V = TypeVar('V')
class Example(Generic[U, V]):
    def __init__(self, a: U, b: V):
        self._a = a
        self._b = b
    
    @classmethod
    def empty(cls):
        return cls(None, None)
from typing import Generic, TypeVar

U = TypeVar('U')
V = TypeVar('V')
class Example(Generic[U, V]):
    def __init__(self, a: U, b: V):
        self._a = a
        self._b = b

    @classmethod
    def both(cls, val: U, b: V):
        return cls(val, b)

But this case returns the following error: error: Argument 2 to "Example" has incompatible type "None"; expected "V" [arg-type]

from typing import Generic, TypeVar

U = TypeVar('U')
V = TypeVar('V')
class Example(Generic[U, V]):
    def __init__(self, a: U, b: V):
        self._a = a
        self._b = b

    @classmethod
    def first(cls, val: U):
        return cls(val, None)

It seems that mypy accepts binding generic inputs to typevars, accepts binding concrete values to infer typevars, but doesn't permit a mix of binding and inferring typevars. What's going on?


Solution

  • tl;dr what if I call type(Example(0, 0)).first(0)? We can't infer cls as type[Example[int, None]] in that case.

    A rambling explanation

    It's typically not type-safe to call a class in Python. The first two examples actually allow type errors because I can subclass them and change the __init__. This type checks but fails at runtime:

    class Maybe1(Maybe[int]):
        def __init__(self):
            super().__init__(0)
    
    Maybe1.empty()
    

    OK, so what if we make Maybe @final? Then Maybe1 can't be defined and the problem goes away. Or does it? We'll come back to this.

    Regarding Example, it's not safe to call cls if it's typed as the enclosing class. So we can either call Example

    class Example(Generic[U, V]):
        def __init__(self, a: U, b: V):
            self._a = a
            self._b = b
    
        @classmethod
        def first(cls, val: U) -> Example[U, None]:
            return Example(val, None)
    

    or type cls as a function

    class Example(Generic[U, V]):
        def __init__(self, a: U, b: V):
            self._a = a
            self._b = b
    
        @classmethod
        def first(
            cls: Callable[[U, None], Example[U, None]],
            val: U
        ) -> Example[U, None]:
            return cls(val, None)
    

    Obviously neither of these will work if you want a subclass's Example1.first to produce an Example1. I think Python's lack of HKTs prohibits typing that properly. It may look something like

    # not valid Python
    
    E = TypeVar("E", bound=Example)
    
    class Example(Generic[U, V]):
        def __init__(self, a: U, b: V):
            self._a = a
            self._b = b
    
        @classmethod
        def first(cls: Callable[[U, None], E[U, None]], val: U) -> E[U, None]:
            return cls(val, None)
    

    I don't believe Self in Python 3.11 helps here

    we reject using Self with type arguments

    We mentioned @final, so what if we @final Example?

    @final
    class Example(Generic[U, V]):
        def __init__(self, a: U, b: V):
            self._a = a
            self._b = b
    
        @classmethod
        def first(cls, val: U) -> Example[U, None]:
            return cls(val, None)
    

    Here's where we recover your error. It's reasonable to ask Python to infer Example in Example.first(0) to be Example[int, None], but what if we do type(Example(0, 0)).first(0)? Then we can't infer type(Example(0, 0)) as Example[int, None], so we can't infer as Example[int, None] in all cases.