pythonpython-typingmypy

How to write mypy-compatible type hints for injecting `typeguard.check_type` into `typing.cast`


The cast(type, obj) is useful but lack of runtime type checking. Therefore, considering the following code:

from types import GenericAlias, UnionType
from typing import Any, cast

from annotated_types import T
from typeguard import check_type

def ensure_type(value, expected_type):  # noqa: ANN401 This function should hold Any value
    """Check the type and return value only if the types are matched.

    Otherwise, raise TypeError.
    """
    if check_type(value, expected_type):
        return cast("T", value)
    msg = f"Type check failed: type({value}) is {type(value)}, expected {expected_type}"
    raise TypeError(msg)

It would be clearer to use obj = ensure_type(..., type) than if check_type(obj, type):

# Using `ensure_type`
data = ensure_type(request_some_data(), dict[str])
key = ensure_type(data.get("key"), PromisedKey)
print(key)

# Using `check_type`
data = request_some_data()
if check_type(data, dict[str]):
    key = data.get("key")
    if check_type(key, PromisedKey):
        print(key)

However, I found it difficult to write type hints and make mypy understand. I guess it might be something like

def ensure_type(value: Any, expected_type: type[T] | GenericAlias | UnionType) -> <the_same_type_of_expected_type>:

Solution

  • Looking at the source for the typeguard package, the check_type function has the signature:

    @overload
    def check_type(
        value: object,
        expected_type: type[T],
        *,
        forward_ref_policy: ForwardRefPolicy = ...,
        typecheck_fail_callback: TypeCheckFailCallback | None = ...,
        collection_check_strategy: CollectionCheckStrategy = ...,
    ) -> T: ...
    
    
    @overload
    def check_type(
        value: object,
        expected_type: Any,
        *,
        forward_ref_policy: ForwardRefPolicy = ...,
        typecheck_fail_callback: TypeCheckFailCallback | None = ...,
        collection_check_strategy: CollectionCheckStrategy = ...,
    ) -> Any: ...
    

    You can use something similar:

    @overload
    def ensure_type(value: object, expected_type: type[T]) -> T: ...
    
    @overload
    def ensure_type(value: object, expected_type: GenericAlias) -> GenericAlias: ...
    
    @overload
    def ensure_type(value: object, expected_type: UnionType) -> UnionType: ...
    
    def ensure_type(value, expected_type):
        """Check the type and return value only if the types are matched.
    
        Otherwise, raise TypeError.
        """
        try:
            return check_type(value, expected_type)
        except TypeError:
            # Override the check_type error message with your custom message.
            msg = f"Type check failed: type({value}) is {type(value)}, expected {expected_type}"
            raise TypeError(msg)