La programación asíncrona revolucionó la forma en que desarrollamos aplicaciones Python. Con la introducción de async y await en Python 3.5 (PEP 492), el lenguaje adquirió una herramienta poderosa para escribir código concurrente de forma legible y eficiente. En esta guía completa, dominarás desde los fundamentos hasta las técnicas avanzadas de programación asíncrona.
Si alguna vez trabajaste con llamadas a API, operaciones de I/O o necesitaste procesar múltiples tareas simultáneamente, dominar async/await es esencial para escribir código Python moderno y de alto rendimiento.
El Problema del Código Síncrono
En el paradigma síncrono tradicional, cada operación bloquea la ejecución hasta completarse. Esto es particularmente problemático para operaciones de I/O, como peticiones HTTP, lectura de archivos o consultas a bases de datos. Mientras el programa espera una respuesta de red, la CPU permanece inactiva, desperdiciando recursos valiosos.
import time
import requests
def obtener_datos(url):
print(f"Obteniendo {url}")
respuesta = requests.get(url)
return respuesta.status_code
inicio = time.time()
codigo1 = obtener_datos("https://api.github.com")
codigo2 = obtener_datos("https://api.github.com/users")
codigo3 = obtener_datos("https://api.github.com/repos")
fin = time.time()
print(f"Tiempo total: {fin - inicio:.2f}s")
En este ejemplo, cada petición espera a que la anterior termine, resultando en un tiempo total igual a la suma de todas las latencias. La programación asíncrona resuelve este problema permitiendo que múltiples operaciones se ejecuten concurrentemente.
¿Qué Son las Corrutinas?
Una corrutina (coroutine) es una función especial que puede pausar su ejecución (await) y reanudarla más tarde. A diferencia de las funciones tradicionales, las corrutinas ceden el control voluntariamente en puntos específicos, permitiendo que otras tareas se ejecuten mientras esperan.
Para definir una corrutina, simplemente usa async def en lugar de def:
async def mi_corrutina():
print("Iniciando...")
await alguna_operacion()
print("¡Finalizado!")
La palabra clave async convierte una función normal en una corrutina. Al llamar a una corrutina, no se ejecuta inmediatamente; en su lugar, devuelve un objeto coroutine que debe ser programado en el event loop. Más detalles sobre la definición de corrutinas están disponibles en la documentación oficial de CPython.
Async y Await en la Práctica
El gran poder de async/await reside en ejecutar operaciones de I/O sin bloquear el hilo principal. Reescribamos el ejemplo anterior usando la biblioteca aiohttp:
import asyncio
import aiohttp
import time
async def obtener_datos(sesion, url):
print(f"Obteniendo {url}")
async with sesion.get(url) as respuesta:
return respuesta.status
async def main():
async with aiohttp.ClientSession() as sesion:
tareas = [
obtener_datos(sesion, "https://api.github.com"),
obtener_datos(sesion, "https://api.github.com/users"),
obtener_datos(sesion, "https://api.github.com/repos"),
]
resultados = await asyncio.gather(*tareas)
print(f"Estado: {resultados}")
inicio = time.time()
asyncio.run(main())
fin = time.time()
print(f"Tiempo total: {fin - inicio:.2f}s")
Observa cómo el tiempo total ahora es aproximadamente igual al de la operación más lenta, no la suma de todas. Esto ocurre porque las peticiones se ejecutan concurrentemente. La especificación completa de async/await está descrita en PEP 492 — Coroutines with async and await syntax.
El Módulo asyncio
asyncio es el módulo estándar de Python para programación asíncrona. Proporciona el event loop, que gestiona la ejecución de corrutinas, tasks y callbacks. El event loop es el corazón de la programación asíncrona, responsable de coordinar cuándo cada tarea debe ejecutarse o pausarse.
import asyncio
async def saludar(nombre, delay):
await asyncio.sleep(delay)
print(f"¡Hola, {nombre}!")
async def main():
tarea1 = asyncio.create_task(saludar("Ana", 2))
tarea2 = asyncio.create_task(saludar("Juan", 1))
tarea3 = asyncio.create_task(saludar("María", 3))
await tarea1
await tarea2
await tarea3
asyncio.run(main())
La función asyncio.run() se introdujo en Python 3.7 y simplifica drásticamente la ejecución de corrutinas. Antes de ella, era necesario gestionar manualmente el event loop con loop.run_until_complete(). La documentación completa del módulo está disponible en docs.python.org/3/library/asyncio.html.
Tasks y Futures
Una Task es un envoltorio alrededor de una corrutina que la programa para ejecución en el event loop. Cuando llamas a asyncio.create_task(), la corrutina se programa automáticamente para ejecución concurrente. Un Future, por otro lado, representa un resultado que estará disponible en el futuro — es un concepto de bajo nivel que rara vez necesitarás usar directamente.
import asyncio
async def operacion_lenta(numero):
await asyncio.sleep(2)
return numero * 2
async def main():
tareas = [
asyncio.create_task(operacion_lenta(1)),
asyncio.create_task(operacion_lenta(2)),
asyncio.create_task(operacion_lenta(3)),
]
resultados = await asyncio.gather(*tareas)
print(f"Resultados: {resultados}")
for i, tarea in enumerate(tareas):
print(f"Tarea {i}: completada={tarea.done()}")
asyncio.run(main())
Las Tasks son fundamentales para ejecutar múltiples operaciones asíncronas en paralelo. La API completa de Tasks se encuentra en la documentación oficial de asyncio tasks.
Ejecución Concurrente con gather y as_completed
asyncio.gather() es la herramienta más común para ejecutar múltiples corrutinas concurrentemente. Devuelve una lista con los resultados en el mismo orden de las corrutinas de entrada. asyncio.as_completed(), por su parte, devuelve resultados a medida que se completan, independientemente del orden original.
import asyncio
async def obtenerdatos(id, delay):
await asyncio.sleep(delay)
return f"Datos del recurso {id_}"
async def main():
coros = [
obtener_datos(1, 3),
obtener_datos(2, 1),
obtener_datos(3, 2),
]
resultados = await asyncio.gather(*coros)
print(f"Gather: {resultados}")
for coro in asyncio.as_completed(
[obtener_datos(4, 3), obtener_datos(5, 1), obtener_datos(6, 2)]
):
resultado = await coro
print(f"Completado: {resultado}")
asyncio.run(main())
Para operaciones más complejas, asyncio.wait() permite controlar cuándo retornar (cuando todas terminen, cuando la primera termine, o cuando la primera falle). Estos patrones se usan ampliamente en aplicaciones reales y están documentados en la sección de ejecución concurrente de tareas.
Timeouts y Manejo de Errores
En aplicaciones reales, es crucial manejar operaciones que pueden tardar más de lo esperado. asyncio.timeout() (disponible desde Python 3.11) y asyncio.wait_for() permiten definir límites de tiempo para tus operaciones asíncronas.
import asyncio
async def operacion_lenta():
await asyncio.sleep(10)
return "¡Completado!"
async def main():
try:
async with asyncio.timeout(3):
resultado = await operacion_lenta()
print(resultado)
except TimeoutError:
print("¡La operación excedió el límite de tiempo!")
try:
resultado = await asyncio.wait_for(
operacion_lenta(), timeout=2
)
print(resultado)
except asyncio.TimeoutError:
print("¡Timeout con wait_for!")
asyncio.run(main())
El manejo de errores en código asíncrono sigue el mismo patrón try/except de Python síncrono, pero es importante notar que las excepciones en tasks deben capturarse explícitamente. PEP 525 — Asynchronous Generators define cómo se comportan los generadores asíncronos con respecto a errores.
Peticiones HTTP Asíncronas con aiohttp
Uno de los usos más comunes de async/await es hacer peticiones HTTP concurrentes. La biblioteca aiohttp es la opción estándar para HTTP asíncrono en Python, ofreciendo soporte completo para clientes y servidores HTTP.
import asyncio
import aiohttp
async def consultar_api(sesion, url):
async with sesion.get(url) as resp:
datos = await resp.json()
return datos
async def main():
urls = [
"https://api.github.com",
"https://api.github.com/users/python",
"https://api.github.com/repos/python/cpython",
]
async with aiohttp.ClientSession() as sesion:
tareas = [consultar_api(sesion, url) for url in urls]
resultados = await asyncio.gather(*tareas)
for url, datos in zip(urls, resultados):
print(f"{url}: {len(datos)} campos")
asyncio.run(main())
aiohttp también soporta sesiones persistentes, conexiones SSL, cookies y autenticación. La documentación completa está disponible en docs.aiohttp.org. Si prefieres una alternativa más moderna, httpx también ofrece soporte async con una API similar a requests.
Trabajando con Archivos Asíncronos
Las operaciones de lectura y escritura de archivos también se benefician de la programación asíncrona, especialmente al trabajar con múltiples archivos grandes. La biblioteca aiofiles proporciona una API asíncrona para operaciones de I/O en archivos.
import asyncio
import aiofiles
async def procesar_archivo(ruta):
async with aiofiles.open(ruta, mode='r') as archivo:
contenido = await archivo.read()
return f"{ruta}: {len(contenido)} caracteres"
async def main():
archivos = ["datos1.txt", "datos2.txt", "datos3.txt"]
tareas = [procesar_archivo(archivo) for archivo in archivos]
resultados = await asyncio.gather(*tareas)
for resultado in resultados:
print(resultado)
asyncio.run(main())
aiofiles es particularmente útil para aplicaciones web que necesitan servir archivos grandes o procesar subidas sin bloquear el event loop. Más información está disponible en el repositorio oficial de aiofiles en GitHub.
Generadores Asíncronos y Comprehensions Asíncronas
Python también soporta generadores asíncronos (PEP 525) y comprehensions asíncronos (PEP 530), que permiten producir y consumir secuencias de forma asíncrona. Son útiles para procesar flujos de datos o paginar resultados de APIs.
import asyncio
async def generar_numeros():
for i in range(5):
await asyncio.sleep(0.5)
yield i
async def main():
async for numero in generar_numeros():
print(f"Recibido: {numero}")
resultados = [x async for x in generar_numeros()]
print(f"Resultados: {resultados}")
asyncio.run(main())
Los generadores asíncronos implementan los protocolos __aiter__ y __anext__, y son fundamentales para bibliotecas de streaming y procesamiento de datos en tiempo real. La especificación completa está disponible en PEP 530 — Asynchronous Comprehensions.
Mejores Prácticas con Async/Await
Escribir código asíncrono eficiente requiere atención a algunos principios fundamentales:
1. Usa asyncio.run() en lugar de gestionar el event loop manualmente
asyncio.run() gestiona la creación y cierre del event loop automáticamente, además de manejar la limpieza de recursos.
# Correcto
asyncio.run(main())
Incorrecto (versiones antiguas)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()
2. Evita mezclar código síncrono y asíncrono sin necesidad
Llamar funciones síncronas dentro de corrutinas bloquea el event loop. Si necesitas ejecutar código síncrono, usa asyncio.to_thread() para ejecutarlo en un hilo separado.
import asyncio
import requests
async def obtener_datos_incorrecto():
¡Esto BLOQUEA el event loop!
respuesta = requests.get("https://api.github.com")
return respuesta.json()
async def obtener_datos_correcto():
Esto NO bloquea el event loop
loop = asyncio.get_running_loop()
respuesta = await loop.run_in_executor(
None, requests.get, "https://api.github.com"
)
return respuesta.json()
3. Usa siempre async context managers para recursos
Las sesiones HTTP, conexiones de bases de datos y archivos deben gestionarse con async with para garantizar la liberación adecuada de recursos.
async with aiohttp.ClientSession() as sesion:
async with sesion.get(url) as respuesta:
return await respuesta.json()
4. Cuidado con el exceso de tareas concurrentes
Crear miles de tasks simultáneamente puede sobrecargar el event loop. Usa asyncio.Semaphore para limitar el número de operaciones concurrentes.
import asyncio
async def procesar_con_semaforo():
semaforo = asyncio.Semaphore(10)
async def tarea_limitada(item):
async with semaforo:
await asyncio.sleep(1)
return item * 2
tareas = [tarea_limitada(i) for i in range(100)]
return await asyncio.gather(*tareas)
Para profundizar en patrones de concurrencia, te recomendamos el artículo Async IO in Python de Real Python, que ofrece una excelente introducción al tema con ejemplos prácticos.
Además, si quieres entender cómo los decoradores pueden ayudar a crear código asíncrono más limpio, consulta nuestra guía completa sobre Decoradores en Python.
Async/Await en Frameworks Web
Los principales frameworks web Python modernos soportan async/await de forma nativa. FastAPI fue construido desde cero con soporte async, mientras que Django (desde la versión 3.1) y Flask (desde la versión 2.0) también ofrecen soporte para vistas asíncronas. La combinación de async/await con frameworks web permite servir miles de peticiones simultáneas con recursos mínimos.
Para dominar los generadores en Python, otra herramienta esencial para procesamiento de datos en streaming, accede a nuestro artículo sobre Generadores en Python.
Conclusión
La programación asíncrona con async/await transformó la forma en que escribimos Python para operaciones de I/O. Con el módulo asyncio, bibliotecas como aiohttp y aiofiles, y las mejores prácticas presentadas en esta guía, estás preparado para escribir aplicaciones Python rápidas, eficientes y escalables.
Recuerda: async/await no se trata de paralelismo real (múltiples hilos o procesos), sino de concurrencia gestionada por un único event loop. Para tareas con uso intensivo de CPU, considera usar multiprocessing o bibliotecas como concurrent.futures.
Para continuar tus estudios, explora la documentación oficial de asyncio, practica con proyectos reales y experimenta con diferentes bibliotecas asíncronas. El ecosistema async de Python crece rápidamente, y dominar estos conceptos es un diferenciador competitivo en el mercado de desarrollo.