I am having a function safe_cast
which casts a value to a given type, but raises if the value fails to comply with the type at runtime:
from typing import TypeVar
T = TypeVar('T')
def safe_cast(t: type[T], value: Any) -> T:
if isinstance(value, t):
return cast(T, value)
raise TypeError()
This works nicely with primitive types. But I run into problems if I want to safe_cast
against a UnionType:
string = "string"
casted: str | int = safe_cast(str | int, string)
The instance check works with a union type. But my solution does not work, because mypy gives me
error: Argument 1 to "safe_cast" has incompatible type "UnionType"; expected "Type[<nothing>]" [arg-type]
I figure that <nothing>
refers to the unspecified type variable T
here. I also figure that apparently mypy cannot resolve Union[str, int]
to Type[T]
. My question is: How can I solve this?
I looked into creating an overload for the UnionType. IIUC, in order to write the overload, I would need to create a Generic Union Type with a variadic number of arguments. I failed to get this done.
Is this the right direction? If yes, how do I get it done? If no, how can I solve my problem with safe_cast
ing Union types?
typing.cast
is likely to be special-cased in a type-checking implementation, because while this works in mypy,
import typing as t
if t.TYPE_CHECKING:
reveal_type(t.cast(str | int, "string")) # mypy: Revealed type is "Union[builtins.str, builtins.int]"
the type annotations for typing.cast
don't actually do anything meaningful with union types:
@overload
def cast(typ: Type[_T], val: Any) -> _T: ...
@overload
def cast(typ: str, val: Any) -> Any: ...
@overload
def cast(typ: object, val: Any) -> Any: ...
What you'd actually want to do is to exploit the magic that your type-checker-implementation already offers. This is very easily done by making typing.cast
your typing API, while making your safe_cast
the runtime implementation.
import typing as t
T = t.TypeVar("T")
if t.TYPE_CHECKING:
from typing import cast as safe_cast
else:
# Type annotations don't actually matter here, but
# are nice to provide for readability purposes.
def safe_cast(t: type[T], value: t.Any) -> T:
if isinstance(value, t):
return value
raise TypeError
>>> reveal_type(safe_cast(int | str, "string")) # mypy: Revealed type is "Union[builtins.int, builtins.str]"