inspect.markcoroutinefunction() permite marcar una función normal para que los mecanismos de inspección la reconozcan como función de corrutina. Es útil cuando un wrapper está escrito con def, pero devuelve de forma constante un objeto awaitable creado por otra función asíncrona. Frameworks web, sistemas de plugins, inyectores de dependencias y runners de pruebas suelen inspeccionar el callable antes de ejecutarlo, por lo que una clasificación incorrecta puede enviar la función por el flujo equivocado.
El problema que resuelve
Una función declarada con async def normalmente es reconocida por inspect.iscoroutinefunction(). Un wrapper síncrono que solo devuelve una corrutina es distinto: al llamarlo produce un objeto que debe esperarse con await, pero la inspección del wrapper puede indicar que no es asíncrono.
import inspect
async def obtener_valor():
return 42
def wrapper():
return obtener_valor()
print(inspect.iscoroutinefunction(obtener_valor))
print(inspect.iscoroutinefunction(wrapper))Esta diferencia puede provocar corrutinas no esperadas, respuestas incorrectas, ejecución en threads innecesarios o fallos difíciles de diagnosticar.
Uso básico
Aplica la marca al wrapper y sigue devolviendo un awaitable real.
import inspect
async def obtener_valor():
return 42
@inspect.markcoroutinefunction
def wrapper():
return obtener_valor()
print(inspect.iscoroutinefunction(wrapper))La marca no convierte el cuerpo, no inicia un event loop y no añade await. Solo cambia cómo el callable es clasificado por código compatible. La implementación debe garantizar que todas las llamadas válidas devuelvan un objeto esperable.
Función de corrutina y objeto corrutina
Una función de corrutina es el callable, normalmente creado con async def. Un objeto corrutina es el valor producido al invocarlo. inspect.iscoroutinefunction() analiza la función; inspect.iscoroutine() analiza el resultado.
import inspect
async def tarea():
return "ok"
resultado = tarea()
print(inspect.iscoroutinefunction(tarea))
print(inspect.iscoroutine(resultado))
resultado.close()El cierre explícito solo evita un aviso en este ejemplo. En una aplicación real, lo habitual es usar await tarea().
Por qué mantener un wrapper síncrono
Un wrapper con async def suele ser más claro, pero a veces una API externa, un descriptor, la generación dinámica de callables o un adaptador de firmas obliga a conservar def. En esos casos, la marca comunica la naturaleza asíncrona a las herramientas de introspección.
import inspect
async def procesar(valor):
return valor * 2
def crear_adaptador(funcion):
@inspect.markcoroutinefunction
def adaptador(*args, **kwargs):
return funcion(*args, **kwargs)
return adaptador
adaptado = crear_adaptador(procesar)El contrato debe ser estable. Una función marcada que a veces devuelve un valor común y otras veces un awaitable resulta impredecible.
Decorators y metadatos
Los decorators pueden ocultar la naturaleza de la función original. Usa functools.wraps para conservar nombre, documentación, anotaciones y referencia al callable envuelto.
from functools import wraps
import inspect
def registrar(funcion):
@wraps(funcion)
@inspect.markcoroutinefunction
def wrapper(*args, **kwargs):
print(f"Llamando {funcion.__name__}")
return funcion(*args, **kwargs)
return wrapper
@registrar
async def cargar_usuario(id_usuario):
return {"id": id_usuario}El orden de los decorators puede influir en los atributos finales. Prueba el callable ya decorado y no solo la función original.
Integración con frameworks
Muchos frameworks deciden si deben hacer await, ejecutar directamente o mover el trabajo a un thread a partir de la inspección. Un wrapper no reconocido puede seguir el flujo síncrono y devolver una corrutina sin esperar. La marca ayuda cuando el framework usa inspect.iscoroutinefunction() o una lógica equivalente.
No todos los frameworks respetan la misma señal. Algunos inspeccionan el valor de retorno o usan atributos propios. Consulta la documentación y crea una prueba de integración. También puedes revisar asyncio en Python, contextlib.chdir en Python, loop_factory en pruebas asyncio y TaskGroup eager_start en Python.
Conservar la firma
Herramientas de documentación, validadores e inyectores pueden usar inspect.signature(). functools.wraps es un buen punto de partida. Solo define __signature__ manualmente si el wrapper realmente expone esa firma, porque una firma artificial puede engañar a usuarios y herramientas.
Momento de las excepciones
El wrapper síncrono suele crear y devolver la corrutina. Los errores ocurridos dentro de la ejecución asíncrona aparecen cuando el llamador espera el resultado, no necesariamente cuando el wrapper retorna.
@inspect.markcoroutinefunction
def wrapper(*args, **kwargs):
return operacion_asincrona(*args, **kwargs)Si necesitas medir toda la ejecución, capturar excepciones asíncronas o garantizar limpieza, un wrapper con async def es más adecuado.
Pruebas correctas
Prueba la clasificación, el objeto devuelto, el valor final, las excepciones y la cancelación.
import inspect
import pytest
def test_clasificacion():
assert inspect.iscoroutinefunction(wrapper)
@pytest.mark.asyncio
async def test_resultado():
valor = await wrapper()
assert valor == 42Una prueba que solo verifica la marca no garantiza que el callable devuelva realmente un awaitable.
Errores comunes
El primer error es pensar que la marca convierte resultados normales en corrutinas. El segundo es usarla en una función inconsistente. El tercero es olvidar que el llamador todavía necesita await. El cuarto es asumir que todos los frameworks usan la inspección estándar. El quinto es ignorar la versión mínima de Python del proyecto.
Alternativa con async def
Cuando sea posible, usa un wrapper asíncrono explícito.
from functools import wraps
def registrar(funcion):
@wraps(funcion)
async def wrapper(*args, **kwargs):
print("inicio")
try:
return await funcion(*args, **kwargs)
finally:
print("fin")
return wrapperEste formato expresa mejor la intención, se detecta de forma natural y permite controlar toda la ejecución.
Type hints
Un tipo como Callable[..., Awaitable[T]] describe bien el contrato. En decorators genéricos, ParamSpec y TypeVar ayudan a conservar parámetros y retorno. La marca en runtime y el tipado estático resuelven problemas diferentes.
Límites del event loop
No uses asyncio.run() dentro de un wrapper reutilizable para fingir una API síncrona. Fallará si ya existe un loop activo y cambia la semántica de cancelación. Tampoco crees una tarea silenciosamente si el contrato promete una corrutina.
Cancelación y contexto
Devolver la corrutina original suele preservar cancelación y contextvars. Crear tasks, cambiar de thread o envolver en futuros puede alterar esas reglas. Un adaptador fino suele ser más seguro.
Seguridad en sistemas de plugins
La inspección puede decidir rutas, permisos y entornos de ejecución. No marques callables desconocidos solo para superar una validación. Verifica el origen, confirma que el retorno sea awaitable y limita el protocolo aceptado.
Compatibilidad entre versiones
Declara la versión mínima de Python y prueba en todos los runtimes compatibles. El comportamiento de introspección y los frameworks puede evolucionar. Consulta la documentación oficial de inspect y la documentación oficial de asyncio.
Lista práctica
Usa la marca solo cuando el wrapper deba seguir siendo síncrono, garantiza un awaitable en cada llamada, conserva metadatos, documenta el uso de await, prueba con el framework real, valida excepciones y cancelación, y evita gestionar el event loop de forma oculta.
Conclusión
inspect.markcoroutinefunction() resuelve un caso concreto: un callable regular que devuelve siempre un awaitable y debe ser reconocido como función de corrutina. No sustituye a async def ni corrige un wrapper inconsistente. Utilizada con tipado, documentación y pruebas, mejora la interoperabilidad entre decorators, frameworks, plugins y herramientas de introspección.







