Search code examples
pythonpython-typingpyright

Cannot use None as TypeVar constraint?


Using TypeVar one can annotate functions like:

def f[T: (str, int)](x:T) -> T:
    if isinstance(x, int):
        return 0
    return x

and type checkers will be happy. However it does not work for a None constraint.

def f[T: (str, None)](x:T) -> T:
    if x is None:
        return None
    return x

gives the error Type "None" is not assignable to type "T@f".

Why is this not possible? Is there a way to achieve this with TypeVars or does one have to use overloads?

Edit: The examples used to say return 'a', which would raise a valid error from the type checker. This was an oversight I made while trimming down the example for this question. Also it is indeed something that I ran into with pyright, not mypy (mypy doesn't complain).


Solution

  • This is either a bug or a limitation of Pyright/Pylance, which is the type checker in question.

    x is None adds a condition on the original type variable T, causing x to be of type None* within the if block. To oversimplify things, None* can be understood as "some subtype of None and T".

    (playground)

    def f[T: (str, None)](x: T) -> T:
        if x is None:
            reveal_type(x)  # None*
            return None
    
        reveal_type(x)  # str*
        return 'a'
    

    In reality, None/NoneType is a final type, so its only subtype is itself. Pyright doesn't understand this well enough, and so it wrongly considers None not to be assignable to T, which at the time is None*.