Watch at EaseHistorias. Respuestas. Perspectiva.

Tecnología · GUÍA PRÁCTICA

¿asyncio.run() falla en Jupyter o FastAPI? Corrige al dueño del bucle, no el síntoma

La excepción avisa de un conflicto de propiedad: algo ya ejecuta el bucle en este hilo. Espera dentro de ese dueño o crea un solo bucle en la verdadera entrada síncrona.

Matriz original para elegir asyncio.run, await o entradas separadas según el dueño del bucle.
Ilustración conceptual original: identifica primero al dueño del bucle y después el traspaso. Watch at Ease

Python lanza este error antes de iniciar un segundo bucle en el mismo hilo. Es intencional: asyncio.run() posee la creación, ejecución, limpieza de generadores asíncronos, cierre del ejecutor y cierre del bucle. Un kernel de Notebook o un servidor web ya posee esas responsabilidades.

La reparación segura depende del llamador. En una celda o ruta asíncrona, espera la corrutina. En un script independiente, conserva un solo asyncio.run() en el límite del proceso. En una biblioteca reutilizable, expón una API asíncrona y haz que el envoltorio síncrono falle si ya existe un bucle.

Nombra al dueño antes de cambiar código

Registra el hilo, el resultado de asyncio.get_running_loop() y el host. Una celda, un manejador web, un gancho de ciclo de vida y una entrada CLI tienen contratos distintos.

No llames stop(), close(), run_until_complete() ni asyncio.run() sobre un bucle que no creaste.

Usa await dentro de notebooks y marcos asíncronos

En Jupyter cambia result = asyncio.run(work()) por result = await work(). En FastAPI el llamador asíncrono debe esperar dentro del bucle del marco.

Vincula las Task largas al ciclo de vida del host y define inicio, cancelación y cierre.

Conserva un único dueño superior en un script

La entrada real puede usar asyncio.run(main()); si varias llamadas comparten bucle y contexto, usa un solo asyncio.Runner en secuencia.

No repartas runners entre auxiliares; la implementación interior debe seguir siendo esperable.

Separa los contratos asíncrono y síncrono

Implementa async def fetch_async(...). El envoltorio fetch(...) solo usa asyncio.run() sin un bucle activo; si lo hay, debe indicar await fetch_async.

Comprueba el bucle antes de construir la corrutina y cierra explícitamente cualquier corrutina sin usar.

Envía entre hilos solo con un límite real

Para un bucle en otro hilo, asyncio.run_coroutine_threadsafe(coro, loop) devuelve un Future; define también tiempos, cancelación y cierre.

Esperar future.result() desde el hilo del bucle puede causar interbloqueo.

Limita los parches a una capa de compatibilidad

Si son necesarios, limita alcance y versión y pruébalos como adaptador; no los uses como valor global predeterminado.

Un valor devuelto no prueba integridad: pueden quedar errores de contexto, tareas pendientes o fallos posteriores.

Demuestra el límite reparado

Prueba la API asíncrona bajo un bucle activo y el envoltorio síncrono en un hilo normal; verifica valores, errores, cancelación, contextvars, repeticiones y asyncio.all_tasks().

La prueba negativa debe exigir que el envoltorio síncrono falle de inmediato dentro del bucle y recomiende await.

Diagrama original de una implementación asíncrona con entradas separadas que evitan anidar bucles.
Ilustración conceptual original: una implementación sirve a límites síncronos y asíncronos explícitos. Watch at Ease