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.
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.
defiscoroutinefunction(obj):
marker_name, marker_value=_get_coroutinefunction_marker()
whileTrue: # unwrap & checkifismethod(obj):
obj=obj.__func__continueifmarker_valueisnotMISSING:
ifgetattr(obj, marker_name, MISSING) ismarker_value:
returnTrueifisinstance(obj, partial):
obj=obj.funccontinueimpl=getattr(obj, _partialmethod_attribute_name, MISSING)
ifisinstance(impl, partialmethod):
obj=impl.funccontinue# unlike its namesake in the `inspect` module, it does not unwrapreturn_has_code_flag(obj, CO_COROUTINE)
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.
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
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.
Activity
x42005e1f commented on Dec 9, 2025
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:
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
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_markpythongh-142418: Fix inspect.iscoroutinefunction for marked functools…
Fix NEWS entry formatting for pythongh-142418
pythongh-142418: Remove unnecessary cycle detection from _has_corouti…
x42005e1f commented on Dec 10, 2025
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.
pythongh-142418: Simplify _has_coroutine_mark by removing unnecessary…
pythongh-142418: Restore __partialmethod__ handling in _has_coroutine…