With the code below
def incr[T: int | None](value: T) -> T:
if value is None:
return value
return value + 1
incr(None)
incr(1)
Running mypy gives error:
main.py:4: error: Incompatible return value type (got "int", expected "T") [return-value] Found 1 error in 1 file (checked 1 source file)
Why is this happening? It appears to me that the line is returning correct type. Note: the goal of such type annotation was to show that the function returns None only when given None as input
T: int | None
is a type variable with an upper bound. It indicates that if you pass any subtype of int | None
, you will get that subtype back.
mypy is warning you that if you pass a subclass of int
, the function behaves wrongly at runtime. This is due to the definition of builtins.int__add__
, which returns int
(rather than typing.Self
):
import typing as t
def incr[T: int | None](value: T) -> T:
if value is None:
return value
return value + 1 # error: Incompatible return value type (got "int", expected "T") [return-value]
class MyInt(int):
pass
>>> result: t.Final = incr(MyInt(3))
>>> if t.TYPE_CHECKING:
... reveal_type(result) # note: Revealed type is "MyInt"
>>> print(type(result))
<class 'int'>
The solution is to use constrained type variables, which behaves by up-casting the result to the appropriate constrained type:
def incr[T: (int, None)](value: T) -> T: ...
>>> if t.TYPE_CHECKING:
... reveal_type(result) # note: Revealed type is "builtins.int"