
Coroutines y async/await: Tu Primer Código Asíncrono en Python
Tabla de Contenido
En los artículos anteriores construimos la base teórica: concurrencia vs paralelismo
, procesos, hilos, el GIL y el event loop
. Ahora es momento de escribir código. En este artículo vamos a aprender a crear coroutines, usar las palabras clave async y await, y entender exactamente qué pasa cuando pausamos y reanudamos la ejecución.
¿Qué es una Coroutine?
Una coroutine es como una función normal de Python, pero con un superpoder: puede pausar su ejecución cuando encuentra una operación que podría tardar (una petición de red, una lectura de disco, una espera). Mientras la coroutine está pausada, el event loop puede ejecutar otras tareas. Cuando la operación termina, la coroutine se “despierta” y continúa desde donde se detuvo.
Esta capacidad de pausar y reanudar es lo que le da concurrencia a nuestra aplicación.
Para trabajar con coroutines necesitamos dos palabras clave:
async def→ define una coroutine.await→ pausa la coroutine hasta que la operación esperada termine.
Creando Coroutines
Crear una coroutine es casi idéntico a crear una función normal. La única diferencia es usar async def en lugar de def:
async def mi_coroutine() -> None:
print('¡Hola mundo!')
Info
Llamar a una coroutine no la ejecuta. Solo produce un objeto coroutine. Para ejecutarla, necesitas un event loop.
# ❌ Esto NO ejecuta la coroutine
mi_coroutine() # Retorna un objeto coroutine, no imprime nada
# ✅ Esto SÍ la ejecuta
import asyncio
asyncio.run(mi_coroutine()) # Imprime: ¡Hola mundo!
Ejecutando Coroutines con asyncio.run
asyncio.run es el punto de entrada principal de cualquier aplicación asyncio. Veamos un ejemplo concreto:
import asyncio
async def sumar_uno(numero: int) -> int:
return numero + 1
resultado = asyncio.run(sumar_uno(1))
print(resultado) # 2
asyncio.run hace varias cosas por nosotros:
- Crea un event loop nuevo.
- Ejecuta la coroutine que le pasamos hasta que se completa.
- Retorna el resultado de la coroutine.
- Limpia cualquier tarea pendiente y cierra el event loop.
Info
asyncio.run está diseñado para ejecutarse una sola vez como entrada principal de tu aplicación. Esa única coroutine debería encargarse de lanzar todas las demás tareas.
Pausando la Ejecución con await
El verdadero poder de asyncio está en await. Esta palabra clave hace dos cosas:
- Ejecuta la coroutine que le sigue (a diferencia de llamarla directamente, que solo crea el objeto).
- Pausa la coroutine contenedora hasta que la coroutine esperada retorne un resultado.
Cuando la coroutine esperada termina, la coroutine contenedora se “despierta” y puede usar el resultado.
import asyncio
async def obtener_saludo() -> str:
return '¡Hola desde una coroutine!'
async def main() -> None:
saludo = await obtener_saludo() # Pausa main() hasta obtener el resultado
print(saludo)
asyncio.run(main())
Simulando Operaciones Largas con asyncio.sleep
Para simular una operación de I/O de larga duración (como una petición HTTP o una consulta a base de datos), podemos usar asyncio.sleep. Esta función pausa la coroutine por el número de segundos indicado, cediendo el control al event loop.
Veamos un ejemplo más completo con una función auxiliar delay:
import asyncio
async def delay(seconds: int) -> str:
print(f'Esperando {seconds} segundo(s)...')
await asyncio.sleep(seconds)
print(f'¡Espera de {seconds} segundo(s) completada!')
return f'Esperé {seconds} segundo(s)'
La Trampa del await Secuencial
Ahora veamos un error muy común al empezar con asyncio. Considera este código:
import asyncio
async def sumar_uno(numero: int) -> int:
return numero + 1
async def mensaje_hola_mundo() -> str:
await asyncio.sleep(1)
return '¡Hola Mundo!'
async def main() -> None:
mensaje = await mensaje_hola_mundo() # Pausa 1 segundo
uno_mas_uno = await sumar_uno(1) # Ejecuta después de la pausa
print(uno_mas_uno)
print(mensaje)
asyncio.run(main())
Cuando ejecutamos este código, pasa 1 segundo antes de que se impriman ambos resultados. Pero sumar_uno(1) es instantánea — ¿por qué espera?
Warning
await pausa toda la coroutine contenedora. No se ejecuta ningún otro código dentro de main() hasta que la expresión await retorne un valor. Como mensaje_hola_mundo() tarda 1 segundo, main() queda pausada ese segundo completo antes de siquiera llegar a sumar_uno(1).
El siguiente diagrama muestra exactamente qué pasa en cada momento:
El problema visualizado
El flujo de ejecución es completamente secuencial, a pesar de usar async/await:
Lo que queremos es que sumar_uno(1) se ejecute mientras mensaje_hola_mundo() espera su segundo de delay. Pero con await secuencial eso no es posible — cada await bloquea la coroutine hasta completarse.
¿Cómo logramos concurrencia real?
Para ejecutar múltiples coroutines de forma concurrente necesitamos herramientas como asyncio.gather o asyncio.create_task. Un adelanto rápido:
async def main() -> None:
# ✅ Ambas coroutines corren concurrentemente
mensaje, uno_mas_uno = await asyncio.gather(
mensaje_hola_mundo(),
sumar_uno(1)
)
print(uno_mas_uno)
print(mensaje)
Con gather, el event loop puede ejecutar sumar_uno(1) mientras mensaje_hola_mundo() está pausada esperando su sleep. El tiempo total se mantiene en ~1 segundo en lugar de sumar ambos tiempos.
En conclusión
async defdefine una coroutine — una función que puede pausarse.awaitpausa la coroutine actual hasta que la operación esperada termine.asyncio.runcrea el event loop y ejecuta la coroutine principal.asyncio.sleepsimula operaciones de I/O de larga duración.- Usar
awaitde forma secuencial no da concurrencia — para eso necesitamosasyncio.gatheroasyncio.create_task.


