markcoroutinefunction: Detect Async Wrappers

Published on: October 10, 2026
Reading time: 5 minutes
Python asynchronous code on a laptop for inspect.markcoroutinefunction

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 == 42

A 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 wrapper

This 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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Python code for traversing folders and files with Path.walk
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Path.walk: Traverse Directories Safely

    Learn Python Path.walk to traverse directories, filter files, handle errors, and control directory-tree processing safely.

    Ler mais

    Tempo de leitura: 5 minutos
    10/10/2026
    Python process debugging in a terminal with source code
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    pdb -p: Debug Running Python Processes

    Learn how to attach pdb to a running Python process, inspect stacks, and diagnose hangs safely.

    Ler mais

    Tempo de leitura: 6 minutos
    09/10/2026
    Python programming code for exact fractions
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    fractions.from_number: Convert Numbers to Fractions

    Learn Python fractions.from_number for exact rational conversion, float and Decimal handling, approximation, validation, and safe arithmetic.

    Ler mais

    Tempo de leitura: 4 minutos
    09/10/2026
    Developer configuring a Python HTTPS server and TLS certificate
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    HTTPSServer: Build a Local HTTPS Server in Python

    Learn Python HTTPSServer for local HTTPS services, TLS certificates, threaded handling, testing, and practical security limits.

    Ler mais

    Tempo de leitura: 4 minutos
    08/10/2026
    Protected files representing secure TAR extraction with Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    tarfile extraction_filter: Extract TAR Safely

    Learn Python tarfile extraction_filter for safer TAR extraction, path validation, links, permissions, and resource limits.

    Ler mais

    Tempo de leitura: 5 minutos
    08/10/2026
    Python code showing warnings controlled with catch_warnings
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    catch_warnings: Capture Python Warnings in Tests

    Learn Python catch_warnings to capture, test, and control warnings with focused filters and safe temporary scopes.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026