Skip to content

inspect.is*function() overloads do not match the runtime behavior #15293

Closed

Description

@x42005e1f

TypeGuard-based overloads for inspect.is*function() were added in #8057, but they are somewhat controversial.

First, the first overload defines the return type via collections.abc.<Object>, while the rest define it via types.<Object>Type. As a result, type checkers infer the narrower type when the known return type differs from collections.abc.<Object>:

from collections.abc import Coroutine
from inspect import iscoroutinefunction
from types import CoroutineType
from typing import reveal_type

def native_coroutine_factory() -> CoroutineType[None, None, None]: ...
def coroutine_factory() -> Coroutine[None, None, None]: ...
def object_factory() -> object: ...

if iscoroutinefunction(native_coroutine_factory):
    reveal_type(native_coroutine_factory)
    # () -> CoroutineType[None, None, None]

if iscoroutinefunction(coroutine_factory):
    reveal_type(coroutine_factory)
    # () -> Coroutine[None, None, None]

if iscoroutinefunction(object_factory):
    reveal_type(object_factory)
    # () -> CoroutineType[Any, Any, Any]

Second, inspect.is*function() also returns True for functions that return a non-native object (for example, if the function was compiled via Cython), which means that the return type may not be types.<Object>Type:

#!/usr/bin/env python3

import inspect


def generator_function():
    yield


async def coroutine_function():
    pass


async def async_generator_function():
    yield


def main():
    # `True` on both CPython and Cython:
    print(inspect.isgeneratorfunction(generator_function))
    print(inspect.iscoroutinefunction(coroutine_function))
    print(inspect.isasyncgenfunction(async_generator_function))

    # `True` on CPython, `False` on Cython:
    print(inspect.isgenerator(generator_function()))
    print(inspect.iscoroutine(coro := coroutine_function()))
    print(inspect.isasyncgen(async_generator_function()))

    coro.close()  # to avoid `RuntimeWarning`

if __name__ == "__main__":
    main()

Third, I am also concerned about the Python core developer's statement that iscoroutinefunction() is basically defined for callable objects that return awaitable objects. Does this mean that we should use Awaitable as the return type instead of Coroutine for inspect.iscoroutinefunction() (since if users follow that definition, inspect.markcoroutinefunction() will be applied just as broadly, even though this somewhat contradicts the documentation)?

Activity

  1. srittau commented on Jan 23, 2026

    @srittau
    Collaborator

    Cc @AlexWaygood and @JelleZijlstra who were involved in the original PR.

  2. JelleZijlstra commented on Jan 23, 2026

    @JelleZijlstra
    Member

    Looking at the issues one by one:

    • I'm not sure what the point of the first overload is. Feel free to try to remove it and see if our CI (including mypy-primer) flags anything.
    • Agree the other overloads should use Coroutine, not CoroutineType. There is no requirement that these return native coroutines.
    • As for Coroutine vs. Awaitable, https://docs.python.org/3.14/library/inspect.html#inspect.markcoroutinefunction says it should be used to mark things that return a Coroutine, so I think we're within our rights to require that such functions return a Coroutine, not just an Awaitable.
  3. x42005e1f commented on Jan 23, 2026

    @x42005e1f
    ContributorAuthor
    • I'm not sure what the point of the first overload is.

    I believe that the first overload has two purposes:

    1. To preserve the original type when it is a subclass.
    2. To preserve the original type subscription.

    If we replace CoroutineType with Coroutine, the output of my first example will look like this:

    if iscoroutinefunction(native_coroutine_factory):
        reveal_type(native_coroutine_factory)
        # () -> CoroutineType[None, None, None]
    
    if iscoroutinefunction(coroutine_factory):
        reveal_type(coroutine_factory)
        # () -> Coroutine[None, None, None]
    
    if iscoroutinefunction(object_factory):
        reveal_type(object_factory)
        # () -> Coroutine[Any, Any, Any]
        # before: () -> CoroutineType[Any, Any, Any]

    But if we also remove the first overload, it will affect the first two cases:

    if iscoroutinefunction(native_coroutine_factory):
        reveal_type(native_coroutine_factory)
        # () -> Coroutine[Any, Any, None]  <-- via Awaitable
        # before: () -> CoroutineType[None, None, None]
    
    if iscoroutinefunction(coroutine_factory):
        reveal_type(coroutine_factory)
        # () -> Coroutine[Any, Any, None]  <-- via Awaitable
        # before: () -> Coroutine[None, None, None]
    
    if iscoroutinefunction(object_factory):
        reveal_type(object_factory)
        # () -> Coroutine[Any, Any, Any]
        # before: () -> CoroutineType[Any, Any, Any]

    This is definitely not the expected behavior, which means that the first overload must remain. I think replacing types.<Object>Type with collections.abc.<Object> will suffice.

  4. added a commit that references this issue on Feb 2, 2026
  5. nightcityblade commented on Aug 21, 2026

    @nightcityblade
    Contributor

    Hi, I'd like to work on this. I'll update the fallback overloads to use the corresponding collections.abc return protocols and add focused regression tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions