Skip to content

TypeGuard does not correctly narrow down generics #11907

@bibajz

Description

@bibajz

Bug Report

I wrote a higher-order function which converts a function into a coroutine function and also is idempotent on a coroutine function.

I wanted to try TypeGuard as it seemed to fit this particular use case.

To Reproduce

import typing as t

from typing_extensions import ParamSpec, TypeGuard

T = t.TypeVar("T")
P = ParamSpec("P")


def is_awaitable(
    coro_or_value: t.Union[T, t.Awaitable[T]]
) -> TypeGuard[t.Awaitable[T]]:
    return hasattr(coro_or_value, "__await__")


def ensure_async(
    fn: t.Union[t.Callable[P, T], t.Callable[P, t.Awaitable[T]]]
) -> t.Callable[P, t.Awaitable[T]]:
    async def inner(*args: P.args, **kwargs: P.kwargs) -> T:
        coro_or_value = fn(*args, **kwargs)
        if is_awaitable(coro_or_value):
            return await coro_or_value  # mypy says: Incompatible return value type (got "T", expected "T")
        else:
            return coro_or_value  # mypy says: Incompatible return value type (got "Union[T, Awaitable[T]]", expected "T")

    return inner


def add(x: int, y: int) -> int:
    return x + y


async def a_add(x: float, y: float) -> float:
    return x + y


if t.TYPE_CHECKING:
    reveal_type(ensure_async(add))  # Revealed type is "def (*<nothing>, **<nothing>) -> typing.Awaitable[<nothing>]"
    reveal_type(ensure_async(add)(1, 2))  # Revealed type is "typing.Awaitable[<nothing>]"
    reveal_type(ensure_async(a_add))  # Revealed type is "def (*<nothing>, **<nothing>) -> typing.Awaitable[<nothing>]"

Expected Behavior

  • In the case of Incompatible return value type (got "T", expected "T"), I would not expect this message.
  • In the case of Incompatible return value type (got "Union[T, Awaitable[T]]", expected "T"), I would expect that this is what TypeGuard is precisely for - knows how to handle the else case of type narrowing, so no message.

Actual Behavior

Mypy is not able to typecheck. The important errors:

2022_01_05_typeguard_mypy_bug.py:21: error: Incompatible return value type (got "T", expected "T")
2022_01_05_typeguard_mypy_bug.py:23: error: Incompatible return value type (got "Union[T, Awaitable[T]]", expected "T")

Full mypy log, including the exemplary functions:

2022_01_05_typeguard_mypy_bug.py:21: error: Incompatible return value type (got "T", expected "T")
2022_01_05_typeguard_mypy_bug.py:23: error: Incompatible return value type (got "Union[T, Awaitable[T]]", expected "T")
2022_01_05_typeguard_mypy_bug.py:37: note: Revealed type is "def (*<nothing>, **<nothing>) -> typing.Awaitable[<nothing>]"
2022_01_05_typeguard_mypy_bug.py:37: error: Argument 1 to "ensure_async" has incompatible type "Callable[[int, int], int]"; expected "Callable[[VarArg(<nothing>), KwArg(<nothing>)], Awaitable[<nothing>]]"
2022_01_05_typeguard_mypy_bug.py:38: note: Revealed type is "typing.Awaitable[<nothing>]"
2022_01_05_typeguard_mypy_bug.py:38: error: Argument 1 to "ensure_async" has incompatible type "Callable[[int, int], int]"; expected "Callable[[VarArg(<nothing>), KwArg(<nothing>)], Awaitable[<nothing>]]"
2022_01_05_typeguard_mypy_bug.py:38: error: Argument 1 has incompatible type "int"; expected <nothing>
2022_01_05_typeguard_mypy_bug.py:38: error: Argument 2 has incompatible type "int"; expected <nothing>
2022_01_05_typeguard_mypy_bug.py:39: note: Revealed type is "def (*<nothing>, **<nothing>) -> typing.Awaitable[<nothing>]"
2022_01_05_typeguard_mypy_bug.py:39: error: Argument 1 to "ensure_async" has incompatible type "Callable[[float, float], Coroutine[Any, Any, float]]"; expected "Callable[[VarArg(<nothing>), KwArg(<nothing>)], Awaitable[<nothing>]]"
Found 7 errors in 1 file (checked 1 source file)

(Write what happened.)

Your Environment

  • Mypy version used: 0.940+dev.9c05d3d19de74fc10a51aa5b663e6a38bc6abc73
  • Python version used: 3.10.1 and 3.9.7

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugmypy got something wrong

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions