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

Time:11-18

(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?

CodePudding user response:

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.

  • Related