Pregunta 1Dada async def hello(): return 'Hi', ¿qué devuelve hello()?
Fundamentos de async / await — Acelera tus programas aprovechando el tiempo de espera
Aprende corrutinas, bucle de eventos y asyncio.sleep, y cómo async def / await delegan la espera de I/O a otra tarea para acelerar la ejecución.
async / await es un mecanismo para hacer otro trabajo mientras esperas operaciones de E/S (Entrada/Salida — lectura de archivos, llamadas de red, consultas a la BD y otras operaciones dominadas por el tiempo de espera). Puede acelerar tareas como llamar 100 veces a una Web API, sin salir de un solo hilo. Este artículo recorre tres ideas centrales: las corrutinas, el bucle de eventos y asyncio.sleep.
Sobre la ejecución de código aquí
async / await es todo cuestión de tiempos, pero el runner de este sitio acumula la salida de print y la muestra toda de golpe cuando termina el script. La salida en tiempo real y la sensación del tiempo transcurrido no coincidirán con un entorno Python real. El artículo usa diagramas para dejar claro el comportamiento interno, pero si quieres ver el flujo de print en vivo o sentir los tiempos reales, ejecútalo en tu entorno Python local con asyncio.run.
Proceso e hilo — el contexto de async / await
Antes de meternos con async / await, fijemos los conceptos de proceso e hilo.
Un proceso (un programa en ejecución desde el punto de vista del SO) es una unidad de ejecución independiente con su propio espacio de memoria. Ejecutas un script Python y obtienes un proceso; ejecutas otro en otra terminal y tienes dos.
Un hilo (el flujo que realmente ejecuta el código dentro de un proceso) es la unidad que usa la CPU para hacer avanzar el código. Un proceso puede tener varios hilos, pero un programa Python típico corre como 1 proceso + 1 hilo, y una espera como time.sleep detiene ese hilo.
- Una unidad de ejecución independiente desde la vista del SO
- Tiene su propio espacio de memoria
- La unidad que realmente ejecuta el código dentro de un proceso
- Python normal suele tener solo uno corriendo
- Se turna dentro de un mismo hilo
- Puedes crear cuantas quieras con
async / await
threading (añade hilos) y multiprocessing (añade procesos).async / await solo gestiona las corrutinas más internas. Cambia entre corrutinas en el mismo hilo, sin añadir hilos ni CPUs — solo cede el control a otra corrutina mientras la CPU está ociosa esperando E/S. Para ejecutar hilos verdaderamente en paralelo usa threading; para añadir procesos usa multiprocessing (los siguientes dos artículos los cubren).
Qué es una corrutina — la estrella de async / await
Una corrutina (una función / objeto definida con async def que puede pausarse en puntos await internos) está en el corazón de async / await. Una función normal corre de principio a fin cuando la llamas, pero una corrutina puede pausarse en cada await interno y reanudarse cuando el bucle de eventos se lo indica. Esa es la base sobre la que se construye async.
Llamar a una función async def no ejecuta su cuerpo — solo devuelve un objeto corrutina (por ejemplo, coro = hello()). El cuerpo solo empieza a ejecutarse cuando haces await coro o se lo pasas a asyncio.run(coro).
import asyncio
async def hello():
return "Hi"
# Llamarla no ejecuta el cuerpo
coro = hello()
print(coro) # <coroutine object hello at 0x...>
# ↑ 0x... es una dirección de memoria.
# Solo significa "se creó una corrutina"
# await la ejecuta de verdad
print(await coro) # Hi
return la termina. Los await internos pausan y reanudan en el medio.Cuando escribes await some_io(), la corrutina se pausa y cambia a otra tarea, y otras corrutinas pueden ejecutarse mientras espera — esa es la característica que define a async.
Qué es el bucle de eventos — el planificador que cambia entre corrutinas
El bucle de eventos (un planificador que cambia entre corrutinas por turnos) es lo que asyncio ejecuta por debajo. Repite sin fin: toma una corrutina de la cola → la ejecuta → cambia a otra en await → vuelve a poner en la cola las esperas completadas.
import asyncio
async def main():
print("start")
await asyncio.sleep(0) # sleep(0) en realidad no espera,
# solo marca "aquí se puede cambiar"
print("end")
# asyncio.run inicia el bucle → corre main → cierra el bucle al terminar
asyncio.run(main())
# Salida:
# start
# end
Todo esto pasa en un solo hilo — sin núcleos de CPU adicionales, solo rellenando el tiempo de espera ocioso con otra corrutina. En el Python del navegador de este sitio, el bucle de eventos ya está corriendo, así que puedes escribir await directamente en el nivel superior sin llamar a asyncio.run(...).
Por qué async / await — cambiar a otra tarea durante las esperas
En el código síncrono (sync) (el estilo normal línea por línea), time.sleep(1) bloquea el programa — la CPU está ociosa, pero el programa está congelado. Lo mismo pasa con las respuestas de Web APIs, las consultas a la BD y la finalización de E/S de archivos.
async / await te permite cambiar a otra tarea allí donde escribas "esperar", manteniéndote en el mismo hilo pero cambiando en el momento en que arranca una espera.
# requests = cliente HTTP síncrono (una llamada a la vez)
# httpx = cliente HTTP con soporte async (await en llamadas paralelas)
import requests, asyncio, httpx
# Sync: pega 3 APIs una por una → 3 segundos en total
def fetch_users_sync():
r1 = requests.get("https://api.example.com/users/1") # ← espera 1 seg
r2 = requests.get("https://api.example.com/users/2") # ← otro 1 seg
r3 = requests.get("https://api.example.com/users/3") # ← otro 1 seg
return [r1.json(), r2.json(), r3.json()]
# Async: pega las 3 en paralelo → termina en ~1 seg (la más lenta)
async def fetch_users_async():
async with httpx.AsyncClient() as client:
r1, r2, r3 = await asyncio.gather(
client.get("https://api.example.com/users/1"),
client.get("https://api.example.com/users/2"),
client.get("https://api.example.com/users/3"),
)
return [r1.json(), r2.json(), r3.json()]
Los detalles de asyncio.gather vienen después
El asyncio.gather(...) que se usa aquí ejecuta varias corrutinas en concurrencia y espera todos los resultados. La semántica detallada — valores de retorno, manejo de excepciones — se cubre en el siguiente artículo: Tasks de asyncio.
Concurrente, no paralelo
async / await nunca añade CPU — el código que satura la CPU (cómputo pesado) no se acelerará. Solo rellena el tiempo de CPU ocioso durante "espera de E/S / espera de red / sleep" cambiando a otra tarea. Esto es ejecución concurrente, no ejecución paralela real. Para correr de verdad en varios núcleos necesitas threading o multiprocessing — cubiertos en los siguientes dos artículos.
async def y await — lo básico
Una función definida con async def se llama función corrutina — llamarla no ejecuta el cuerpo, solo devuelve un objeto corrutina. Para ejecutarla de verdad, hazle await o pásasela a asyncio.run().
await x significa "espera a que x termine, cambiando a otra tarea mientras tanto". x puede ser una de tres cosas: una corrutina (en lo que se centra este artículo), una Task o un Future (el objeto Task que vemos en el siguiente artículo, más el objeto de notificación de finalización que usa internamente) — en el código del día a día usarás sobre todo corrutinas o Tasks.
import asyncio
# Define una función corrutina con async def
async def hello():
return "Hi"
# Llamarla solo devuelve un objeto corrutina (el cuerpo no se ejecuta)
print(hello()) # <coroutine object hello at 0x...>
# await la ejecuta (el await de nivel superior funciona en este entorno del navegador)
result = await hello()
print(result) # Hi
# En un script Python real, envuélvelo con asyncio.run()
# print(asyncio.run(hello())) # Hi
| Elemento | Significado | Notas |
|---|---|---|
| async def f(): | Define una función corrutina | Llamarla no ejecuta el cuerpo |
| f() | Crea un objeto corrutina | Necesita await para correr de verdad |
| await f() | Espera la finalización, cambiando a otras | Solo legal dentro de una función async |
| asyncio.sleep(N) | Espera N segundos (cediendo durante la espera) | No bloquea como time.sleep |
| asyncio.run(f()) | Ejecuta desde el nivel superior | Punto de entrada estándar en Python real |
Sin await, la corrutina nunca corre
Si escribes hello() solo, el cuerpo no se ejecuta nunca y verás un aviso como <coroutine object hello at 0x...> en la consola. Haz siempre await hello() o ejecútala vía asyncio.run(hello()). "Llamar" y "ejecutar" son dos cosas distintas en async — no las confundas.
La ejecución concurrente comparte la espera — la recompensa de asyncio
asyncio.sleep(seconds) es la versión async de sleep — cambia a otra tarea durante la espera. Combinada con asyncio.gather (siguiente artículo) para correr varias corrutinas a la vez, todos los sleeps avanzan en concurrencia, así que el tiempo total se vuelve el sleep individual más largo (no la suma, como te daría time.sleep).
Internamente, asyncio.sleep(N) registra "reanudar esta corrutina en N segundos" en el bucle y cambia fuera de inmediato. En cambio, time.sleep(N) es una llamada al SO que bloquea por completo la CPU — el bucle también se detiene y ninguna otra corrutina puede correr. Usar time.sleep dentro de una función async tira por la borda toda la idea de async, así que cuidado.
Verificación de conocimientos
Responde cada pregunta una a una.
Pregunta 2Dentro de una función async, ¿cuál "espera N segundos cambiando a otra tarea"?
Pregunta 3¿Qué carga de trabajo se beneficia más de async / await?