Skip to content

Inconsistency in Cython object handling #143908

@x42005e1f

Description

@x42005e1f

Bug report

Bug description:

There seems to be no consensus in the codebase on how to handle Cython objects. Both inspect.iscoroutinefunction() and inspect.iscoroutine() are defined in the documentation for async def functions and their objects, respectively. Therefore, the following code should be valid for them:

#!/usr/bin/env python3

import inspect


async def coroutine_function() -> None:
    pass


def main() -> None:
    print(inspect.iscoroutinefunction(coroutine_function))  # `True`
    print(inspect.iscoroutine(coro := coroutine_function()))  # `True`
    coro.close()  # to avoid `RuntimeWarning`


if __name__ == "__main__":
    main()

The same semantics are also expected by typeshed, which defines annotations as follows (and thus declares support for introspection):

...
@overload
def iscoroutinefunction(obj: object) -> TypeGuard[Callable[..., CoroutineType[Any, Any, Any]]]: ...
...
def iscoroutine(object: object) -> TypeIs[CoroutineType[Any, Any, Any]]: ...
...

However, after cythonize -i -3, the code changes its behavior:

True  # `inspect.iscoroutinefunction()`
False  # `inspect.iscoroutine()`

This is because Cython uses its own objects for compiled functions and generators/coroutines/asynchronous generators. inspect.is<object>() returns True only for uncompiled objects, as it relies on the types module, but inspect.is<object>function() relies on checking function flags and handles function-like objects in a special way (for compatibility with Cython). As a result, we get the contradictory "coroutine function does not return a coroutine".

The problem is even deeper than one might imagine:

  1. Cython objects do not actually correspond completely to their counterparts from CPython. See [BUG] inspect.get*state() does not work for compiled objects on Python ≥3.11 cython/cython#7448.
  2. Mypy avoids using the types module to infer the return type due to possible incompatibility with Cython. See mypy should use CoroutineType instead of Coroutine for return types of async def functions mypy#18635 and Allow types.GeneratorType to be used instead of collections.abc.Generator as the return type of generator functions mypy#20522.
  3. If the user wants to handle objects regardless of whether the module has been compiled or not, they have to come up with their own way of checking objects. See Add a collection.abc.Function ABC for function-like objects #131983.

What if we want to define a universal decorator that works for both regular and asynchronous functions? We could use inspect.iscoroutinefunction(), but it has very limited detection (in particular, it cannot detect object with async def __call__()), and library developers will only be able to rely on inspect.markcoroutinefunction() after the Python 3.11's EOF. A frequently suggested alternative is to check the return value's type using inspect.isawaitable(), but this is actually shooting oneself in the foot, as it will lead to false positives for future-like objects (such as asyncio future objects, which are also awaitable). The most logical solution would be to use inspect.iscoroutine(), but it does not support Cython (and in fact, it is even less useful; see GrahamDumpleton/wrapt#236 (comment)).

It seems to me that the question of whether the standard library (and related tools) should be Cython-aware has not been fully explored, which leads to such contradictions. I marked the issue as a bug, since it violates the expected behavior for any consistent interpretation.

CPython versions tested on:

3.9, 3.10, 3.11, 3.12, 3.13, 3.14

Operating systems tested on:

Linux

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