pythoncastingmypypython-typing

Type hint for a cast-like function that raises if casting is not possible


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_casting Union types?


Solution

  • 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]"