How to create a TypeGuard that mimics isinstance

113 views Asked by At

I have to check an object that may have been created by an API. When I try using isinstance(obj, MyClass) it get a TypeError if obj was created by the API.

I wrote a custom function to handle this.

def is_instance(obj: Any, class_or_tuple: Any) -> bool:
    try:
        return isinstance(obj, class_or_tuple)
    except TypeError:
        return False

The issue I am having is using is_instance() instead of the builtin isinstance() does not have any TypeGuard support, so the type checker complains.

def my_process(api_obj: int | str) -> None:
    if is_instance(api_obj, int):
        process_int(api_obj)
    else:
        process_str(api_obj)

"Type int | str cannot be assigned to parameter ..."

How could I create a TypeGuard for this function?

1

There are 1 answers

11
blhsing On BEST ANSWER

You can annotate is_instance with a TypeGuard that narrows the type to that of the second argument. To handle a tuple of types or such tuples as class_or_tuple, use a type alias that allows either a type or a tuple of the type alias itself:

T = TypeVar('T')
ClassInfo: TypeAlias = type[T] | tuple['ClassInfo', ...]

def is_instance(obj: Any, class_or_tuple: ClassInfo) -> TypeGuard[T]:
    try:
        return isinstance(obj, class_or_tuple)
    except TypeError:
        return False

But then, as @user2357112 points out in the comment, TypeGuard isn't just meant for narrowing by type, but also value, so failing a check of is_instance(api_obj, int) doesn't mean to the type checker that api_obj is necessarily str, so using an else clause would not work:

def my_process(api_obj: int | str) -> None:
    if is_instance(api_obj, int):
        process_int(api_obj)
    else:
        # mypy complains: Argument 1 to "process_str" has incompatible type "int | str"; expected "str"  [arg-type]
        process_str(api_obj)

so in this case you would have to work around it with a redundant call of is_instance(api_obj, str):

def my_process(api_obj: int | str) -> None:
    if is_instance(api_obj, int):
        process_int(api_obj)
    elif is_instance(api_obj, str):
        process_str(api_obj)

Demo with mypy: https://mypy-play.net/?mypy=latest&python=3.12&gist=4cea456751dff62c3e0bc998b74462f5

Demo of type narrowing with a tuple of types with mypy: https://mypy-play.net/?mypy=latest&python=3.12&gist=98ca0795a315e541c4b1b9376d81812f

EDIT: For PyRight as you requested in the comment, you would have to make the type alias generic:

T = TypeVar('T')
ClassInfo: TypeAlias = Union[Type[T] , Tuple['ClassInfo[T]', ...]]

def is_instance(obj: Any, class_or_tuple: ClassInfo[T]) -> TypeGuard[T]:
    try:
        return isinstance(obj, class_or_tuple)
    except TypeError:
        return False

This would make mypy complain, however, so it's really down to different type checkers having different interpretations to the Python typing rules.

Demo with PyRight here