async / await: escribir código asíncrono con estilo síncrono
await pausa la corrutina actual y cede control al event loop hasta que la tarea termine. El resultado: código que parece síncrono pero no bloquea el hilo.
await dentro de corrutinas
await suspende la corrutina actual y devuelve el control al event loop. El event loop puede entonces ejecutar otras corrutinas mientras la suspendida espera. Cuando la operación awaitable termina, la corrutina se reanuda.
import asyncio
async def obtener_usuario(user_id: int) -> dict:
print(f" → Consultando usuario {user_id}...")
await asyncio.sleep(1) # simula consulta a DB
return {"id": user_id, "nombre": f"Usuario {user_id}"}
async def obtener_pedidos(user_id: int) -> list[dict]:
print(f" → Consultando pedidos de {user_id}...")
await asyncio.sleep(1) # simula consulta a DB
return [{"pedido": 1, "total": 50.0}]
async def perfil_completo(user_id: int) -> dict:
# En secuencia: 2 segundos (await espera cada uno)
usuario = await obtener_usuario(user_id)
pedidos = await obtener_pedidos(user_id)
return {"usuario": usuario, "pedidos": pedidos}
async def main() -> None:
perfil = await perfil_completo(42)
print(perfil)
asyncio.run(main()) → Consultando usuario 42...
→ Consultando pedidos de 42...
{'usuario': {'id': 42, 'nombre': 'Usuario 42'}, 'pedidos': [{'pedido': 1, 'total': 50.0}]}Cualquier objeto que implemente __await__ puede ser awaitable: corrutinas, asyncio.Task, asyncio.Future, y objetos de librerías como aiohttp.
await expr es equivalente a pedirle al event loop: «pausa aquí, ve a hacer otras cosas, y cuando expr termine, regresa y continúa desde este punto». El event loop mantiene el estado de la corrutina suspendida en memoria.
Crear y ejecutar tareas con asyncio.create_task
Para ejecutar corrutinas de forma concurrente sin esperar a que terminen inmediatamente, usa asyncio.create_task(). Esto registra la corrutina en el event loop y la empieza a ejecutar lo antes posible.
import asyncio
import time
async def descargar(nombre: str, segundos: float) -> str:
print(f"Iniciando: {nombre}")
await asyncio.sleep(segundos)
print(f"Completado: {nombre}")
return f"datos de {nombre}"
async def main() -> None:
inicio = time.perf_counter()
# create_task registra las corrutinas y las inicia concurrentemente
tarea1 = asyncio.create_task(descargar("usuarios", 2.0))
tarea2 = asyncio.create_task(descargar("productos", 1.0))
tarea3 = asyncio.create_task(descargar("pedidos", 1.5))
# await en la tarea espera a que esa tarea específica termine
r1 = await tarea1
r2 = await tarea2
r3 = await tarea3
fin = time.perf_counter()
print(f"Resultados: {r1}, {r2}, {r3}")
print(f"Tiempo: {fin - inicio:.1f}s") # ~2.0s, no 4.5s
asyncio.run(main())Iniciando: usuarios
Iniciando: productos
Iniciando: pedidos
Completado: productos
Completado: pedidos
Completado: usuarios
Resultados: datos de usuarios, datos de productos, datos de pedidos
Tiempo: 2.0screate_task es más flexible: puedes crear las tareas en distintos momentos y decidir cuándo esperarlas. gather es más conciso cuando tienes todas las corrutinas listas al mismo tiempo. En la práctica, gather es más común para el caso simple.
Resultados y retornos
Las corrutinas retornan valores como funciones normales — el valor de retorno llega cuando await la corrutina termina.
import asyncio
async def calcular_precio(producto_id: int) -> float:
await asyncio.sleep(0.5) # simula consulta
precios = {1: 29.99, 2: 49.99, 3: 9.99}
return precios.get(producto_id, 0.0)
async def calcular_total(ids: list[int]) -> float:
# Crea tareas para todos los productos
tareas = [asyncio.create_task(calcular_precio(pid)) for pid in ids]
total = 0.0
for tarea in tareas:
precio = await tarea # espera cada tarea y obtiene su resultado
total += precio
return total
async def main() -> None:
total = await calcular_total([1, 2, 3])
print(f"Total del carrito: ${total:.2f}")
# Total del carrito: $89.97
asyncio.run(main())Total del carrito: $89.97Cancelar tareas
Las tareas pueden cancelarse llamando a tarea.cancel(). La corrutina recibirá una excepción asyncio.CancelledError en su próximo punto de suspensión (await).
import asyncio
async def tarea_larga(nombre: str) -> str:
try:
print(f"{nombre}: iniciando (tarda 10s)")
await asyncio.sleep(10)
return f"{nombre}: completado"
except asyncio.CancelledError:
print(f"{nombre}: cancelado — limpiando recursos")
raise # siempre re-lanza CancelledError
async def main() -> None:
tarea = asyncio.create_task(tarea_larga("proceso-pesado"))
# Esperamos 1 segundo y luego cancelamos
await asyncio.sleep(1)
tarea.cancel()
try:
resultado = await tarea
except asyncio.CancelledError:
print("La tarea fue cancelada correctamente")
asyncio.run(main())proceso-pesado: iniciando (tarda 10s)
proceso-pesado: cancelado — limpiando recursos
La tarea fue cancelada correctamenteDentro de una corrutina, cuando capturas CancelledError para limpiar recursos, debes re-lanzarla con raise. Si la tragas silenciosamente, la tarea no se marca como cancelada y el event loop puede quedar en un estado inconsistente.