Let's say I have a function that takes only an argument and is guaranteed to return a value of the same type as the argument. Inside, depending on the specific type of the argument, it will apply some transformations or others.
A minimal example could be something like this:
from typing import TypeVar
T = TypeVar("T")
S = TypeVar('S', bound=str)
def str_transform(s: S) -> S:
return s
def take_any_transform_only_str(obj: T) -> T:
if isinstance(obj, str):
return str_transform(obj)
return obj
It seems straightforward that take_any_transform_only_str
will always return a value of the same type than the argument it was given. And yet, mypy
(v1.15.0
) complains with:
test.py:11: error: Incompatible return value type (got "str", expected "T") [return-value]
What would be the correct way to annotate this function?
(Probably I could use typing.overload
, but I'd very much prefer a solution based on type variables than having to manually annotate the different cases)
Let's say I have a function that takes only an argument and is guaranteed to return a value of the same type as the argument. Inside, depending on the specific type of the argument, it will apply some transformations or others.
A minimal example could be something like this:
from typing import TypeVar
T = TypeVar("T")
S = TypeVar('S', bound=str)
def str_transform(s: S) -> S:
return s
def take_any_transform_only_str(obj: T) -> T:
if isinstance(obj, str):
return str_transform(obj)
return obj
It seems straightforward that take_any_transform_only_str
will always return a value of the same type than the argument it was given. And yet, mypy
(v1.15.0
) complains with:
test.py:11: error: Incompatible return value type (got "str", expected "T") [return-value]
What would be the correct way to annotate this function?
(Probably I could use typing.overload
, but I'd very much prefer a solution based on type variables than having to manually annotate the different cases)
1 Answer
Reset to default 2mypy
does not track the relationship between the types of symbols. Unfortunately, it means that it is unable to detect that T
is str
or a subclass of str
in the isinstance()
branch of your function.
If you do not want to use overload, then you could try a typing.cast()
. It is a useful escape hatch when you can determine something that a type checker is unable to. The type checker just takes your word that type safety is satisfied. At runtime, cast()
is an identity function that returns its second argument without performing any checks. As such, using cast()
will mask any errors should the return type of str_transform()
be changed. This means using typing.overload
is safer, if more verbose.
Example usage:
return typing.cast(T, str_transform(obj))
Using overload
Using overload
is the safer alternative as it means mypy can alert you if the return type of str_transform()
changes in a way that is incompatible with take_any_transform_only_str()
. However, it requires a somewhat strange overload to work as desired.
@overload
def take_any_transform_only_str(obj: S) -> S:
# Using `S` over `str` in the overload means mypy will
# preserve the type of subclasses of `str`
pass
@overload
def take_any_transform_only_str(obj: T) -> T: pass
def take_any_transform_only_str(obj: T | str) -> T | str:
# `| str` is an escape hatch to allow returning `str` when the type
# of` obj` has been narrowed to `str`. mypy would complain even if
# the return type was `S`, as `S` is not the same as `str`
if isinstance(obj, str):
return str_transform(obj)
else:
return obj
This preserves the types as desired: eg.
class MyStr(str):
pass
reveal_type(take_any_transform_only_str(123))
# note: Revealed type is "int"
reveal_type(take_any_transform_only_str(''))
# note: Revealed type is "str"
# Shows that types which are subclasses of str are preserverd
reveal_type(take_any_transform_only_str(MyStr()))
# note: Revealed type is "MyStr"
# Also shows that complex types are preserved and allowed
x: int | str = ''
reveal_type(take_any_transform_only_str(x))
# note: Revealed type is "Union[int, str]"
str_transform
modify aLiteral
? If yes, be careful, your type-checker would infer a different literal than you have at runtime. – Daraan Commented Feb 5 at 11:33obj
if it's of a given subtype what T could be – mgab Commented Feb 5 at 12:13# type: ignore[return-type]
as you know the type does not change. I would also prefer it overcast
, as casting is a stronger lie. – Daraan Commented Feb 5 at 13:43