microsoft / pyright

Static Type Checker for Python
Other
13.12k stars 1.4k forks source link

Invalid handling of manually typed NoReturn/Never coroutine #8826

Closed ItsDrike closed 2 weeks ago

ItsDrike commented 2 weeks ago

Describe the bug Pyright is generally able to identify unreachable blocks of code, this works for regular functions that have a return type of NoReturn or Never, but also for async functions with this return type. This logic even works with an explicitly set type-hint a variable as a synchronous function, like: Callable[[], Never] Oddly enough though, it does not work with a type-hint like this for an async function (neither with Coroutine[Any, Any, Never] nor Awaitable[Never])

Code or Screenshots

from collections.abc import Coroutine
from typing import Any, Callable, Never, cast, reveal_type

# Helper functions

async def async_never_func() -> Never:
    while True:
        pass

async def async_normal_func() -> int:
    return 42

def sync_never_func() -> Never:
    while True:
        pass

def sync_normal_func() -> int:
    return 42

# Demonstration

async def a() -> None:
    reveal_type(async_never_func)
    await async_never_func()
    print("x")  # unreachable (as it should be)

async def b() -> None:
    x = cast(Coroutine[Any, Any, Never], async_normal_func())
    reveal_type(x)
    await x
    print("x")  # reachable???

async def c() -> None:
    sync_never_func()
    print("x")  # unreachable (as it should be)

async def d() -> None:
    x = cast(Callable[[], Never], sync_normal_func)
    x()
    print("x")  # unreachable (as it should be)
erictraut commented 2 weeks ago

Pyright is working as intended here, so I don't consider this a bug.

Pyright's reachability analysis honors call expressions where the LHS of the call expression has a declared type. It doesn't honor call expressions where the LHS of the call expression has an inferred type. This is an intentional design decisions that weighs tradeoffs between functionality and performance.

If you want pyright to treat case b as unreachable, you can do the following:

async def b() -> None:
    cr: Callable[[], Coroutine[Any, Any, Never]] = cast(Any, async_normal_func)
    await cr()
    print("x")