GeezerVison is showing github.com — `inspect.iscoroutinefunction()` does not detect marked partial objects · Issue #142418 · python/cpython
inspect.iscoroutinefunction() does not detect marked partial objects #142418
DescriptionBug report
Bug description:
inspect.markcoroutinefunction() is applied directly to the passed function, except for methods (which is correct behavior). However, inspect.iscoroutinefunction() checks the passed function only after unwrapping (a function that is not wrapped in either functools.partial or functools.partialmethod (currently, _has_coroutine_mark() does not handle these objects; why?)), and thus cannot detect the marker that is not at the end of the unwrapping chain, which results in false negative in cases where a functools.partial/functools.partialmethod object is marked.
>>> from functools import partial
>>> from inspect import iscoroutinefunction, markcoroutinefunction
>>> async def wedonotlikesnakecase():
... return "the_funniest_joke_in_the_world"
>>> def manufacturer_of_jokes(somefunc):
... global manufacturer_of_jokes
... del manufacturer_of_jokes
... return somefunc()
>>> joke = partial(manufacturer_of_jokes, wedonotlikesnakecase)
>>> joke = markcoroutinefunction(joke)
>>> iscoroutinefunction(joke)
False
I discovered this problem while theoretically considering backporting inspect.iscoroutinefunction() to older versions of Python. The problem has not yet been caused by any use case, so it is okay if the issue is closed due to lack of demand. But just in case, I am attaching part of how iscoroutinefunction() is implemented in my code.
def iscoroutinefunction(obj):
marker_name, marker_value = _get_coroutinefunction_marker()
while True: # unwrap & check
if ismethod(obj):
obj = obj.__func__
continue
if marker_value is not MISSING:
if getattr(obj, marker_name, MISSING) is marker_value:
return True
if isinstance(obj, partial):
obj = obj.func
continue
impl = getattr(obj, _partialmethod_attribute_name, MISSING)
if isinstance(impl, partialmethod):
obj = impl.func
continue
# unlike its namesake in the `inspect` module, it does not unwrap
return _has_code_flag(obj, CO_COROUTINE)
CPython versions tested on:
3.12, 3.13, 3.14
Operating systems tested on:
Linux
Linked PRs
Activityx42005e1f commented on Dec 9, 2025 ContributorAuthor More actions Some thoughts on this issue.
The inspect.is*function() family is quite... ambiguous. From the meaning of inspect.isfunction(), it can be logically deduced that an object for which inspect.iscoroutinefunction() returns True must also be a function object, and therefore have attributes such as __code__. This is also reinforced by the fact that in #99247, adding a check for classes that have async def __call__() was rejected. However, at the moment, inspect.iscoroutinefunction() handles the following cases:
- Coroutine function-like objects (for compatibility with Cython).
functools.partial objects wrapping a coroutine function-like or a marked object.
- Regular functions defined by
functools.partialmethod for a coroutine function-like.
Can we say that inspect.iscoroutinefunction() is still a check for a coroutine function object? I think not, and that a "coroutine factory" is more appropriate here (where the broader is a "coroutine-like factory", which is true for any callable object that returns an object that passes isinstance(object, (types.CoroutineType, collections.abc.Coroutine)) - a bit redundant, but fast). However, the standard library does not introduce any functions for such objects, and users have to rely on inspect.iscoroutinefunction() in cases where they actually expect any callable object that returns something coroutine-like (note that we even use collections.abc.Coroutine in user-defined annotations!).
The above explains why the lack of support for marked partial objects may be unexpected behavior. I would also add that it is expected that inspect.iscoroutinefunction() returns True for any object to which inspect.markcoroutinefunction() has been applied, which is currently not the case for functools.partial objects. Joshua-Ward1 commented on Dec 10, 2025 Claiming this — I'm implementing a fix now. chaope commented on Dec 10, 2025 Based on the PR that added this feature/marker, the purpose is to
be able to continue to identify sync functions returning awaitables, without calling them to see that.
Given that, I think this issue indeed reports a bug. The root cause is that markcoroutinefunction sets the mark on partial function object (joke), while iscoroutinefunction looks at the underlying function object of partial function object (manufacturer_of_jokes). The deeper issue is that markcoroutinefunction and iscoroutinefunction uses different code path to find the function object it operates on.
In order to fix this bug, we can do it in two ways: fix the problem with minimum change or fix it more thoroughly, but riskier.
-
minimum change: add functools._unwrap_partial to markcoroutinefunction
-
fix it more thoroughly: move the logic in _has_code_flag to a separate function and reuse it in markcoroutinefunction and _has_coroutine_mark
def _find_underlying_function(f):
f = functools._unwrap_partialmethod(f)
while ismethod(f):
f = f.__func__
f = functools._unwrap_partial(f)
return f
def _has_code_flag(f, flag):
f = _find_underlying_function(f)
if not (isfunction(f) or _signature_is_functionlike(f))::
return False
return bool(f.__code__.co_flags & flag)
def markcoroutinefunction(f):
f = _find_underlying_function(f)
f._is_coroutine_marker = _is_coroutine_mark
return f
def _has_coroutine_mark(f):
f = _find_underlying_function(f)
return getattr(f, "_is_coroutine_marker", None) is _is_coroutine_mark
x42005e1f commented on Dec 10, 2025 ContributorAuthor More actions
The deeper issue is that markcoroutinefunction and iscoroutinefunction uses different code path to find the function object it operates on.
They should not use the same code path. manufacturer_of_jokes() accepts an arbitrary callable object (not necessarily a coroutine function), and therefore returns an arbitrary value. It should not be marked as a coroutine function.
MetadataMetadataStatusShow more project fields
DevelopmentNo branches or pull requestsIssue actions
|