inspect.markcoroutinefunction() marks a regular callable so coroutine-function detection treats it as asynchronous. It is useful when a wrapper is written with def but consistently returns an awaitable produced by another async function. Frameworks, routers, dependency injectors, test runners, and plugin systems often inspect callables before invoking them, so an unmarked wrapper may be sent through the wrong execution path.
The problem it solves
A function declared with async def is normally recognized by inspect.iscoroutinefunction(). A synchronous wrapper that merely returns a coroutine is different: calling it produces an awaitable, yet inspecting the wrapper itself may report that it is not a coroutine function.
import inspect
async def fetch_value():
return 42
def wrapper():
return fetch_value()
print(inspect.iscoroutinefunction(fetch_value))
print(inspect.iscoroutinefunction(wrapper))This mismatch matters when software decides whether to await a callable, run it directly, or move it to a worker thread. The result can be an un-awaited coroutine warning, an incorrect response object, or a confusing integration failure.
Basic usage
Apply the marker to the wrapper and keep returning a genuine awaitable.
import inspect
async def fetch_value():
return 42
@inspect.markcoroutinefunction
def wrapper():
return fetch_value()
print(inspect.iscoroutinefunction(wrapper))The marker does not rewrite the function body, start an event loop, or insert an await. It only changes how compatible inspection code classifies the callable. The implementation remains responsible for returning an awaitable on every valid call.
Coroutine function versus coroutine object
A coroutine function is the callable, usually created with async def. A coroutine object is the value returned when that callable is invoked. inspect.iscoroutinefunction() examines the callable, whereas inspect.iscoroutine() examines the produced object.
import inspect
async def job():
return "done"
result = job()
print(inspect.iscoroutinefunction(job))
print(inspect.iscoroutine(result))
result.close()The explicit close only prevents an un-awaited coroutine warning in this demonstration. Production code should normally await the object inside an asynchronous context.
Why use a synchronous wrapper
An async def wrapper is usually clearer, but a synchronous wrapper may be required by a third-party API, dynamic callable generation, signature-preserving adapters, descriptor behavior, or a library that creates different kinds of awaitables. The marker exists for these deliberate designs.
import inspect
async def process(value):
return value * 2
def make_adapter(function):
@inspect.markcoroutinefunction
def adapter(*args, **kwargs):
return function(*args, **kwargs)
return adapter
adapted = make_adapter(process)The contract must be stable. A marked callable that sometimes returns an ordinary value and sometimes returns an awaitable is difficult to reason about and can break consumers.
Decorators and metadata
Decorators often hide whether the wrapped function is asynchronous. Use functools.wraps to preserve the original name, documentation, annotations, and wrapped reference.
from functools import wraps
import inspect
def log_call(function):
@wraps(function)
@inspect.markcoroutinefunction
def wrapper(*args, **kwargs):
print(f"Calling {function.__name__}")
return function(*args, **kwargs)
return wrapper
@log_call
async def load_user(user_id):
return {"id": user_id}Decorator order can affect attributes and framework behavior. Test the final decorated callable rather than assuming the marker survived every layer.
Framework integration
Web frameworks and plugin engines may inspect a handler to choose an async or sync execution path. An unrecognized wrapper can be executed as ordinary code, leaving the returned coroutine un-awaited. Marking helps when the framework relies on inspect.iscoroutinefunction() or compatible logic.
Not every framework honors the same mechanism. Some inspect the return value, use private flags, or maintain their own wrapper types. Read the framework documentation and create an integration test. Related Academify guides include asyncio in Python, contextlib.chdir in Python, loop_factory in asyncio tests, and TaskGroup eager_start.
Signature preservation
Documentation tools, validators, and dependency injection systems may call inspect.signature(). functools.wraps provides a good default, but advanced wrappers sometimes define __signature__. Avoid inventing a signature that does not match actual accepted arguments, because that harms users and automated tooling.
Exception timing
A synchronous wrapper typically creates and returns the coroutine. Exceptions raised later inside asynchronous execution appear when the caller awaits it, not necessarily when the wrapper returns.
@inspect.markcoroutinefunction
def wrapper(*args, **kwargs):
return async_operation(*args, **kwargs)If you must measure the complete execution, catch async exceptions, or guarantee cleanup, an async def wrapper is usually more appropriate because it can place await inside try, except, and finally blocks.
Testing the contract
Test classification, the returned object, the final value, errors, and cancellation.
import inspect
import pytest
def test_classification():
assert inspect.iscoroutinefunction(wrapper)
@pytest.mark.asyncio
async def test_result():
value = await wrapper()
assert value == 42A classification-only test is not enough. A callable may be marked yet return a non-awaitable due to a bug. Executing it protects the real behavioral contract.
Common mistakes
The first mistake is treating the marker as a conversion utility. It does not turn ordinary results into coroutines. The second is marking an inconsistent function. The third is forgetting that callers still need await. The fourth is assuming every framework uses standard inspection. The fifth is ignoring the project’s minimum Python version.
The clearer async alternative
Prefer an explicit async wrapper whenever possible.
from functools import wraps
def log_call(function):
@wraps(function)
async def wrapper(*args, **kwargs):
print("start")
try:
return await function(*args, **kwargs)
finally:
print("finish")
return wrapperThis communicates intent, is naturally detected, and lets the decorator control the full execution. Use markcoroutinefunction when a real architectural constraint requires a synchronous wrapper.
Type hints
A type such as Callable[..., Awaitable[T]] describes the wrapper’s contract. Generic decorators can use ParamSpec and TypeVar to preserve parameters and return types. Runtime marking and static typing solve different problems, so use both where appropriate.
Event-loop boundaries
Do not call asyncio.run() inside a reusable wrapper merely to make async code appear synchronous. It fails when another event loop is already running and changes cancellation semantics. Likewise, do not silently create a task unless the API explicitly promises task scheduling rather than a coroutine result.
Cancellation and context
Returning the original coroutine generally preserves normal cancellation and contextvars behavior. Creating tasks or switching threads changes those semantics. A thin wrapper is often safest because it delegates lifecycle decisions to the caller or framework.
Security and plugin systems
Inspection results may influence routing, permissions, and execution environments. Do not mark unknown callables simply to bypass validation. Validate plugin sources, ensure the result is awaitable, and keep the accepted protocol narrow.
Version compatibility
Declare the minimum supported Python version and test on every supported runtime. Inspection behavior and framework compatibility can evolve. Consult the official inspect documentation and the official asyncio task documentation for current details.
Practical checklist
Use the marker only when the wrapper must remain synchronous, guarantee that every call returns an awaitable, preserve metadata, document the await requirement, test with the actual framework, test errors and cancellation, and avoid hidden event-loop management. These steps turn a subtle introspection feature into a predictable integration tool.
Conclusion
inspect.markcoroutinefunction() addresses a focused problem: a regular callable that consistently returns an awaitable and must be recognized as a coroutine function. It is not a replacement for async def and cannot repair an inconsistent wrapper. Used carefully with type hints, metadata preservation, execution tests, and clear documentation, it improves interoperability between decorators, frameworks, plugin systems, and Python’s introspection tools.







