Volver al catálogo
Season 10 20 Episodios 1h 20m 2026

asyncio

v3.14 — Edición 2026. Un análisis exhaustivo del framework asyncio de Python, que cubre el event loop, las coroutines, la concurrencia estructurada, las primitivas de sincronización y los patrones asíncronos avanzados. Para Python 3.14.

Python Core Programación asíncrona
asyncio
Reproduciendo ahora
Click play to start
0:00
0:00
1
El Event Loop y el modelo mental
Establece tu modelo mental base para asyncio. Aprende cómo el event loop actúa como el director de una orquesta, gestionando trabajos de forma cooperativa sin depender del multithreading.
3m 56s
2
Coroutines frente a Awaitables
Desmitifica las palabras clave async y await. Exploramos la distinción fundamental entre una función coroutine y un objeto coroutine, y qué sucede realmente cuando haces await en una operación.
3m 43s
3
El punto de entrada asyncio.run()
Descubre cómo inicializar una aplicación asyncio de forma segura. Hablamos sobre asyncio.run, el cierre de executors y el gestor de contexto Runner para ciclos de vida complejos del loop.
4m 19s
4
Planificación con Tasks
Aprende a ejecutar operaciones de forma concurrente usando asyncio.create_task(). Descubrimos las graves consecuencias del garbage collection en tareas sin referencia.
3m 42s
5
Concurrencia estructurada con TaskGroups
Domina la concurrencia estructurada. Entiende cómo asyncio.TaskGroup gestiona de forma segura múltiples operaciones concurrentes y garantiza cierres limpios cuando ocurren excepciones.
3m 59s
6
Cancelación de Tasks y Timeouts
Explora la mecánica para abortar operaciones. Aprende por qué se lanza asyncio.CancelledError, cómo manejarlo en un bloque finally y por qué nunca deberías ignorarlo.
4m 13s
7
Ceder el control con Sleep
Entiende el verdadero propósito de asyncio.sleep(0). Descubre cómo ceder el control evita que los bucles con alta carga de CPU saturen el event loop y congelen la aplicación.
3m 54s
8
Sincronización: Locks y Mutexes
Evita las condiciones de carrera en código asíncrono. Exploramos asyncio.Lock, analizamos su naturaleza no segura para hilos (non-thread-safe) y mostramos por qué los locks de threading congelarán tu event loop.
4m 23s
9
Coordinación de estado con Events
Aprende a transmitir señales a múltiples tareas en espera. Explicamos cómo asyncio.Event y asyncio.Condition reemplazan de forma elegante los ineficientes bucles de consulta (polling).
3m 40s
10
Limitar la concurrencia con Semaphores
Protege recursos frágiles y evita baneos por límite de peticiones (rate-limiting). Descubre cómo asyncio.Semaphore limita la ejecución concurrente sin bloquear tu arquitectura.
4m 13s
11
Flujos de trabajo Productor-Consumidor
Desacopla productores rápidos de consumidores lentos de forma segura. Explora asyncio.Queue, la señalización de finalización de tareas y las nuevas mecánicas de apagado (shutdown) para colas.
3m 35s
12
Networking de alto nivel con Streams
Sumérgete en los IO Streams de alto nivel. Hablamos sobre StreamReader, StreamWriter y por qué omitir await writer.drain() puede destruir silenciosamente la memoria de tu servidor.
4m 01s
13
Creación de servidores Async
Construye servidores de red altamente concurrentes. Aprende cómo asyncio.start_server abstrae las conexiones de los clientes, generando una tarea aislada para cada peer.
4m 04s
14
Subprocesses no bloqueantes
Ejecuta comandos de shell de forma asíncrona. Descubre por qué usar el módulo estándar subprocess detiene el event loop, y cómo asyncio.create_subprocess_exec lo soluciona.
4m 05s
15
Futures: El puente de bajo nivel
Descubre la base de las declaraciones await. Examinamos asyncio.Future, su papel como resultado eventual y cómo sirve de puente entre el código heredado basado en callbacks y la sintaxis moderna.
4m 18s
16
Transports y Protocols
Echa un vistazo bajo el capó para ver cómo asyncio se comunica con el sistema operativo. Entiende la relación 1:1 basada en callbacks entre los Transports (cómo se mueven los bytes) y los Protocols (qué significan los bytes).
4m 32s
17
Threading en un mundo Async
Conecta los mundos síncrono y asíncrono. Aprende a delegar código bloqueante pesado de forma segura usando executors y callbacks thread-safe sin bloquear el loop.
3m 39s
18
Generadores Async y limpieza
Evita fugas de recursos con los generadores async. Exploramos por qué la iteración 'async for' puede dejar conexiones colgadas cuando se interrumpe, y cómo aclosing() proporciona seguridad.
4m 06s
19
Dominando el Debug Mode
Atrapa errores de concurrencia al instante. Aprende a usar PYTHONASYNCIODEBUG para perfilar callbacks lentos, descubrir coroutines sin await y localizar excepciones nunca recuperadas.
4m 08s
20
Extensión y Loops personalizados
El gran final. Exploramos la integración avanzada y lo que se necesita para escribir un event loop personalizado o crear una subclase de BaseEventLoop para entornos especializados de alto rendimiento.
4m 12s

Episodios

1

El Event Loop y el modelo mental

3m 56s

Establece tu modelo mental base para asyncio. Aprende cómo el event loop actúa como el director de una orquesta, gestionando trabajos de forma cooperativa sin depender del multithreading.

Descargar
Hola, soy Alex de DEV STORIES DOT EU. asyncio, episodio 1 de 20. Muchos desarrolladores escuchan la palabra asíncrono y asumen que su código se ejecutará en paralelo en varios núcleos de CPU. Pero luego revisan su aplicación y descubren que se ejecuta completamente en un solo thread. El secreto de esta eficiencia sin paralelismo real es el event loop, y entender su modelo mental es la base de asyncio. El event loop es el gestor de ejecución central de cualquier aplicación asyncio. Es exactamente lo que su nombre indica: un loop continuo que comprueba si hay operaciones listas para ejecutarse, las ejecuta y luego busca la siguiente operación. Es vital separar este concepto del multithreading. En un programa multithreaded, el sistema operativo controla la ejecución. El sistema operativo pausará a la fuerza un thread y cambiará a otro para compartir el tiempo de CPU. Los propios threads no tienen control sobre cuándo se pausan. Esto requiere un overhead significativo del sistema para gestionar los context switches y proteger la memoria compartida. El event loop opera con un modelo completamente diferente llamado multitarea cooperativa. Todo se ejecuta secuencialmente en un único thread. El loop nunca interrumpe una operación. En su lugar, confía en que el código devuelva explícitamente el control al loop cuando tiene que esperar a algo. Imagina el event loop como un único chef experto en la cocina de un restaurante muy concurrido. El chef recibe varios pedidos a la vez. Si pone una olla grande de caldo en el fuego para que hierva a fuego lento, no se queda de pie frente al fogón mirando el líquido hasta que termina. Ese enfoque bloquearía toda la cocina y no se cocinaría nada más. En su lugar, el chef enciende el fuego, deja la olla a fuego lento e inmediatamente pasa a picar verduras para otro plato. El chef representa el único thread de ejecución. El event loop es el chef escaneando continuamente la cocina, sabiendo exactamente qué ollas están a fuego lento, a qué sartenes hay que darles la vuelta, y pasando al instante al siguiente trabajo disponible. En tu software, una olla a fuego lento suele ser una operación de input u output. Cuando tu código envía una request a una base de datos, la base de datos tarda un tiempo en procesar la query y devolver los datos. Un programa síncrono tradicional se congelaría y esperaría la response. Con un event loop, la operación registra su request y luego le dice al loop que está esperando. El event loop cambia inmediatamente a otro fragmento de código que sí tiene datos listos para procesar. Cuando la base de datos finalmente responde, la operación original le avisa al event loop de que está lista para reanudarse. El event loop la vuelve a colocar en la queue y reanudará su ejecución tan pronto como el trabajo actual ceda el control. Aquí está la clave. Como el event loop no puede detener una operación a la fuerza, todo el sistema depende por completo de la cooperación. Si un trabajo decide realizar un cálculo matemático masivo sin ceder nunca el control, el event loop se detiene. El único thread está ocupado. En nuestra cocina, esto es el chef decidiendo moler a mano una enorme bolsa de harina mientras ignora todos los demás platos. Las ollas a fuego lento se desbordan, se acumulan nuevos pedidos y la cocina se paraliza. El loop es solo tan eficiente como el código que se ejecuta en su interior. La verdadera eficiencia asíncrona no viene de realizar múltiples cálculos en el mismo instante físico exacto, sino de asegurar que tu único thread nunca desperdicie ni un solo milisegundo inactivo mientras espera al mundo exterior. Si quieres ayudar a que el programa siga adelante, puedes apoyarnos buscando DevStoriesEU en Patreon. ¡Gracias por escuchar, y happy coding a todos!
2

Coroutines frente a Awaitables

3m 43s

Desmitifica las palabras clave async y await. Exploramos la distinción fundamental entre una función coroutine y un objeto coroutine, y qué sucede realmente cuando haces await en una operación.

Descargar
Hola, soy Alex de DEV STORIES DOT EU. asyncio, episodio 2 de 20. Escribes una función, la llamas y no pasa absolutamente nada. Tu código se ejecuta sin errores, pero la base de datos está vacía y la request de red nunca se realiza. El problema es un malentendido fundamental sobre lo que realmente hace llamar a una función asíncrona. Hoy, vamos a ver Coroutines vs Awaitables. En Python normal, cuando llamas a una función estándar, se ejecuta inmediatamente. Las funciones asíncronas rompen esta regla por completo. Hay una diferencia estricta entre una coroutine function y un coroutine object. Cuando escribes async def, estás creando una coroutine function. Cuando llamas a esa función en tu código, no se ejecuta el cuerpo de la función. En su lugar, devuelve un coroutine object. Piénsalo como pedir un café. La función async def es la opción del menú. Llamar a esa función es como hacer tu pedido en la caja. Te dan un ticket. Ese ticket es tu coroutine object. Has dejado clara tu intención, pero todavía no tienes tu bebida, y nadie ha empezado siquiera a prepararla. Para activar realmente el proceso de preparación y tener tu café, tienes que esperar en el mostrador. En Python, esto lo haces usando la keyword await. Cuando escribes await seguido de ese coroutine object, ocurren dos cosas distintas. Primero, la coroutine por fin empieza a ejecutar su código interno. Segundo, la función donde pusiste el await se pausa por completo. Le devuelve el control a Python, indicando que no puede continuar hasta que esta coroutine en concreto termine. Este comportamiento de pausa es la diferencia mecánica central de la programación asíncrona. Mientras tu función está pausada esperando el café, Python es libre de ir a ejecutar otro código en otra parte. Esto nos lleva al término más amplio, awaitable. Un awaitable es simplemente cualquier objeto que Python te permite usar con la keyword await. Todas las coroutines son awaitables. Cuando veas await, léelo como una orden directa: ejecuta este objeto awaitable hasta que termine, y suspende mi progreso actual hasta que devuelva un resultado final. Si escribes una función async llamada fetch data, simplemente llamar a fetch data devuelve el coroutine object. Si asignas esa llamada a una variable llamada pending request, esa variable simplemente guarda la coroutine sin ejecutar. La red permanece completamente inactiva. Más adelante en tu script, cuando escribes await pending request, Python por fin ejecuta la llamada de red. La ejecución de tu bloque de código actual se detiene exactamente en esa línea. Una vez que el servidor responde, la expresión await se resuelve con los datos devueltos, y el código que la rodea continúa en la siguiente línea. Aquí está la clave. Solo puedes usar la keyword await dentro de una función async def. Como hacer await de un objeto requiere pausar la ejecución actual, la función que contiene el await debe ser capaz de pausarse a sí misma. Por eso el comportamiento asíncrono se propaga hacia afuera. Para hacer await de una coroutine, debes estar dentro de una coroutine. Estás construyendo una cadena de operaciones suspendidas, todas esperando a que se resuelva la task de nivel más bajo. Recuerda, llamar a una función async sin hacerle await es solo generar un ticket por un trabajo que en realidad nunca le pediste a nadie que hiciera. El código nunca se ejecutará hasta que le hagas await. Gracias por escucharnos. ¡Hasta la próxima!
3

El punto de entrada asyncio.run()

4m 19s

Descubre cómo inicializar una aplicación asyncio de forma segura. Hablamos sobre asyncio.run, el cierre de executors y el gestor de contexto Runner para ciclos de vida complejos del loop.

Descargar
Hola, soy Alex de DEV STORIES DOT EU. asyncio, episodio 3 de 20. Usar incorrectamente el entry point de tu aplicación asíncrona puede dejar thread executors colgados y async generators sin cerrar. Para evitar resource leaks ocultos, necesitas usar la herramienta adecuada para iniciar y detener tu aplicación, lo que nos lleva al entry point asyncio.run. Muchos desarrolladores intentan erróneamente usar esta herramienta para ejecutar coroutines individuales de forma aleatoria desde código síncrono. Ese no es su propósito. No puedes llamar a la función run cuando otro event loop de asyncio ya se está ejecutando en el mismo thread. Hacerlo provoca inmediatamente un runtime error. Está diseñada específicamente para ser el único entry point de alto nivel para un programa. Piensa en inicializar el main loop de un servidor web que coordina todas las requests de tráfico entrantes. Tienes una función asíncrona central que se enlaza a un puerto de red, configura los request handlers y mantiene el servidor activo. Pasas esa única función principal a la función run. Al hacer esto, asyncio gestiona automáticamente todo el ciclo de vida del event loop. Primero, crea un nuevo event loop y lo establece como el loop activo actual para el thread. A continuación, ejecuta la coroutine principal del servidor web hasta que finaliza. Aquí reside la clave. El trabajo más valioso de esta función se realiza después de que el código principal termina de ejecutarse. Realiza una limpieza exhaustiva. Antes de devolver el control a la parte síncrona del programa, cancela las pending tasks que queden. Luego, cierra de forma segura los background threads en el default executor. Finalmente, finaliza todos los async generators antes de cerrar completamente el event loop. También puedes pasar un debug flag a esta función, lo que fuerza al loop subyacente a ejecutarse en debug mode para ayudar a rastrear problemas de ejecución. Dado que esta función estándar desmonta todo al final, crea un límite rígido. Si tienes un escenario donde necesitas ejecutar varios bloques asíncronos distintos desde código síncrono, pero quieres que compartan el mismo event loop, llamar a la función run estándar una tras otra fallará, ya que se crea y destruye un nuevo loop cada vez. Para esa situación, usas el context manager asyncio Runner. Abres un bloque de contexto usando la sentencia with estándar de Python. Al entrar en este bloque, se inicializa el event loop. Una vez dentro, puedes llamar al propio método run del objeto runner. Le pasas una coroutine, la ejecuta hasta completarse y devuelve el resultado. Puedes llamar a este método run interno varias veces dentro del mismo bloque de contexto. El event loop permanece activo, manteniendo el estado, los datos en caché y las conexiones entre esas llamadas separadas. Puedes configurar el context manager al crearlo pasando un debug flag, o incluso un loop factory personalizado si tu entorno requiere una implementación especializada del event loop. Cuando la ejecución finalmente sale del bloque del context manager, el runner ejecuta exactamente la misma secuencia de teardown que la función independiente. Limpia los executors, finaliza los generators y cierra el loop de forma segura. La estabilidad de tu aplicación depende completamente de cómo se inicia y finaliza. Tanto si usas una sola llamada a función como el context manager, dirigir tu ejecución a través de estos entry points oficiales es la única manera de garantizar que tus recursos asíncronos se desmonten de forma fiable cuando el programa finaliza. Eso es todo por este episodio. Gracias por escuchar, y ¡sigue desarrollando!
4

Planificación con Tasks

3m 42s

Aprende a ejecutar operaciones de forma concurrente usando asyncio.create_task(). Descubrimos las graves consecuencias del garbage collection en tareas sin referencia.

Descargar
Hola, soy Alex de DEV STORIES DOT EU. asyncio, episodio 4 de 20. Inicias un proceso en background para enviar métricas del sistema. Más tarde, revisas tu dashboard y falta la mitad de los datos. No se lanzó ningún error. Tu código simplemente se detuvo en silencio a mitad de la ejecución. Esto pasa porque trataste tu background job como fire-and-forget. Hoy vamos a hablar sobre el Scheduling con Tasks, y por qué siempre debes conservar las cosas que creas. Cuando tienes una coroutine que quieres ejecutar de forma concurrente con otro código, usas la función create task de asyncio. Le pasas tu coroutine a esta función, y asyncio la envuelve en un objeto Task. Esto le dice al event loop que programe la task para su ejecución. La función te devuelve inmediatamente el nuevo objeto Task, permitiendo que tu programa principal siga ejecutándose mientras la task opera en background. Muchos developers llaman a create task e ignoran el valor de retorno. Esto es una trampa enorme. Aquí está la clave. El event loop de asyncio solo mantiene weak references a las tasks que está ejecutando. El propio loop no protege tu task del garbage collector de Python. Si no asignas el objeto Task devuelto a una variable o lo guardas en una estructura de datos, el garbage collector acabará dándose cuenta de que no existen hard references. Cuando eso pasa, Python destruye el objeto task. Le da igual si la coroutine está en mitad de la ejecución de una query a la base de datos o esperando una respuesta de red. La task simplemente desaparece. Piensa en una función async llamada ship metrics. Formatea un payload de datos y envía una request HTTP a un servidor externo. Llamas a create task y le pasas ship metrics, pero no asignas el resultado a nada. La task empieza a ejecutarse. Formatea el payload. Luego llega a la network call y se pausa para esperar una conexión. Mientras está pausada, el garbage collector se ejecuta. El contador de strong references es cero. La task se destruye. El servidor nunca recibe el payload, y tu aplicación nunca registra un error porque la ejecución simplemente dejó de existir. Para evitar esto, siempre debes mantener una strong reference a las tasks que programas. Si estás creando una sola task, asígnala a una variable. Si estás programando múltiples background tasks dentro de un loop, añádelas a un set estándar de Python o a una lista. Mientras ese set exista en memoria, las strong references existen, y el garbage collector dejará en paz a tus tasks en ejecución. Luego puedes usar un callback para eliminar la task de tu set una vez que haya terminado. La función create task también acepta algunos argumentos opcionales. Puedes pasar un string al parámetro name, que asigna un identificador específico a la task. Esto es muy recomendable para hacer debugging, ya que hace mucho más fácil rastrear qué operación específica falló si se lanza una excepción más adelante. También puedes pasar un argumento context para establecer un estado de variable de contexto específico para la task. Tratar las operaciones en background como fire-and-forget acabará quemándote con fallos silenciosos. Si le pides al event loop que ejecute algo, debes mantener una hard reference al objeto resultante hasta que el trabajo haya terminado por completo. ¡Gracias por escuchar, happy coding a todos!
5

Concurrencia estructurada con TaskGroups

3m 59s

Domina la concurrencia estructurada. Entiende cómo asyncio.TaskGroup gestiona de forma segura múltiples operaciones concurrentes y garantiza cierres limpios cuando ocurren excepciones.

Descargar
Hola, soy Alex de DEV STORIES DOT EU. asyncio, episodio 5 de 20. Antes de Python 3.11, lanzar múltiples tareas concurrentes era fácil, pero gestionarlas de forma segura cuando una fallaba era francamente difícil. A menudo, acababas con tareas en segundo plano huérfanas malgastando recursos en silencio. La solución a este lío es la concurrencia estructurada usando TaskGroup. Un TaskGroup es un context manager asíncrono. La gente a veces lo confunde con una lista de tareas estándar, pero es mucho más estricto. Proporciona sólidas garantías de seguridad sobre cómo empiezan y terminan las tareas. Impone la regla de que una rutina padre no puede terminar hasta que todas sus operaciones hijas se hayan completado o cancelado limpiamente. Lo usas abriendo un bloque async with. Dentro de ese bloque, llamas al método create task directamente en el objeto del grupo para iniciar tus operaciones concurrentes. No metes estas tareas en un array estándar para hacerles await manualmente. En su lugar, cuando el código llega al final del bloque async with, el TaskGroup se pausa implícitamente. Espera justo ahí hasta que termina cada tarea generada. El bloque simplemente no saldrá antes de tiempo. Aquí está la clave. El verdadero poder de un TaskGroup reside en cómo gestiona los fallos. Con herramientas antiguas como gather, si iniciabas varias tareas y una lanzaba un error, las demás seguían ejecutándose en segundo plano. Tenías que escribir una lógica compleja de manejo de errores para localizar a las supervivientes y matarlas. Un TaskGroup gestiona esto automáticamente. Imagina el escenario de un web scraper obteniendo datos de tres endpoints de API distintos simultáneamente. Necesitas datos de usuario, posts recientes y alertas del sistema. Abres un TaskGroup y generas tres tareas. Todas empiezan a ejecutarse de forma concurrente por la red. A mitad de la operación, el endpoint de posts recientes da un timeout y lanza un error de conexión. El TaskGroup detecta este fallo inmediatamente. Intercepta el error y envía automáticamente una señal de cancelación a la tarea de datos de usuario y a la tarea de alertas del sistema. Limpia esas operaciones pendientes para que no sigan comiéndose el ancho de banda de la red o la memoria. Las tareas restantes lanzan un cancelled error internamente, reconociendo el cierre. Una vez que todas las tareas restantes se detienen de forma segura, el TaskGroup empaqueta el error de conexión original en una nueva estructura llamada ExceptionGroup, y lo lanza fuera del bloque de contexto. Este comportamiento hace que tu código asíncrono sea totalmente predecible. Si la ejecución pasa el bloque con éxito, sabes a ciencia cierta que todas y cada una de las tareas tuvieron éxito. Si el bloque lanza un ExceptionGroup, sabes que el fallo fue capturado y que todo lo demás se cerró correctamente. Nunca dejas tareas descontroladas ejecutándose en segundo plano. Si necesitas los resultados de las tareas exitosas, puedes recuperarlos directamente de los objetos task que creaste, siempre y cuando se completaran antes de que ocurriera el fallo. Al vincular las tareas a un bloque de ciclo de vida estricto, los TaskGroups garantizan que las operaciones concurrentes entren y salgan de tu aplicación como una sola unidad coordinada. Eso es todo por hoy. Gracias por escuchar, ve a construir algo genial.
6

Cancelación de Tasks y Timeouts

4m 13s

Explora la mecánica para abortar operaciones. Aprende por qué se lanza asyncio.CancelledError, cómo manejarlo en un bloque finally y por qué nunca deberías ignorarlo.

Descargar
Hola, soy Alex de DEV STORIES DOT EU. asyncio, episodio 6 de 20. Has escrito un error handler robusto, capturando todas las excepciones genéricas en tu async worker. Pero ahora, durante el shutdown, tu event loop se atasca con zombie tasks que se niegan a morir. Tu red de seguridad en realidad las está atrapando vivas. Esto es exactamente lo que cubrimos hoy: Task Cancellation y Timeouts. Cuando necesitas detener una task en ejecución, llamas a su método cancel. Esto no termina la task instantáneamente como matar un proceso del sistema. En su lugar, asyncio solicita que se detenga inyectando un error, específicamente un asyncio CancelledError, en la task. Este error se lanza exactamente en el await actual o siguiente de la task. La coroutine entonces desenrolla su stack justo como lo haría con cualquier error estándar de Python. Este mecanismo también es la base para los timeouts. Cuando envuelves una task en una función de timeout y el temporizador expira, el event loop no detiene la task por arte de magia. Simplemente llama a cancel en esa task. La task recibe el CancelledError en su siguiente await, deshace su estado y, finalmente, le dice al timeout wrapper que se ha detenido. Solo entonces el timeout wrapper te lanza un TimeoutError de vuelta. Aquí reside la clave. Desde Python 3.8, CancelledError hereda directamente de BaseException, no de la clase Exception estándar. Esta decisión de diseño evita un error catastrófico específico. Los desarrolladores suelen envolver las operaciones de red o de archivos en bloques try y except que capturan las clases Exception genéricas para evitar un crash. Si CancelledError fuera una Exception estándar, esos bloques capturarían la señal de cancelación. La task tal vez haría un log de un warning, se tragaría la señal y seguiría ejecutándose como un zombie. Al subir CancelledError en la jerarquía a BaseException, Python garantiza que tus error handlers del día a día no interceptarán accidentalmente una petición de cancelación. Entonces, ¿cómo gestionas de forma segura el estado cuando se cancela una task? Recurres a la estructura try y finally. Imagina un servidor web procesando una HTTP request entrante. El usuario pide un informe enorme, pero luego cierra la ventana del navegador. El servidor detecta la desconexión y cancela la request task. Dentro de tu código, actualmente estás haciendo un await de una query de base de datos de larga duración. Ese await de repente lanza un CancelledError. Como metiste tu interacción con la base de datos dentro de un bloque try, la ejecución salta instantáneamente a tu bloque finally. Usas ese bloque finally para hacer un rollback limpio de la transacción pendiente y devolver la conexión de base de datos al pool. Una vez que el bloque finally termina, el CancelledError continúa haciendo bubble up, terminando la task con éxito. A veces un bloque finally no es suficiente. Si absolutamente debes realizar una limpieza asíncrona, como enviar una network request a un microservicio remoto para anunciar la cancelación, puedes capturar explícitamente el CancelledError. Pero si haces esto, debes hacer un re-raise explícito de ese error exacto al final de tu bloque except. No hacer el re-raise rompe la mecánica interna de asyncio. La task parecerá haber terminado con éxito en lugar de ser cancelada, lo que corrompe el estado de tu aplicación y rompe la concurrencia estructurada. La regla a recordar es que la cancelación es una request cooperativa, no un comando kill forzoso, y depende completamente de que las excepciones hagan bubble up intactas. Si deseas apoyar el programa, puedes buscar DevStoriesEU en Patreon. Eso es todo por este episodio. Gracias por escuchar, ¡y sigue programando!
7

Ceder el control con Sleep

3m 54s

Entiende el verdadero propósito de asyncio.sleep(0). Descubre cómo ceder el control evita que los bucles con alta carga de CPU saturen el event loop y congelen la aplicación.

Descargar
Hola, soy Alex de DEV STORIES DOT EU. asyncio, episodio 7 de 20. A veces, el secreto para mantener la capacidad de respuesta de tu servidor de red es decirle a tus tareas más pesadas que hagan un sleep de exactamente cero segundos. Si una función nunca se pausa, toda tu aplicación deja de escuchar al mundo exterior. Para solucionar esto, haces yield del control con sleep. En el framework asyncio, el event loop ejecuta exactamente una task a la vez. Se basa completamente en la multitarea cooperativa. Una task se ejecuta continuamente hasta que se encuentra con la palabra clave await, que actúa como un checkpoint para devolver el control de ejecución al event loop. Si escribes una función async que contiene una operación puramente CPU-bound, creas un cuello de botella. Piensa en hacer el parsing de un payload JSON enorme o transformar miles de strings. No hay puntos de await naturales en un loop de procesamiento de datos estándar. Como la task nunca hace yield, el event loop se queda bloqueado. Cualquier request de red entrante, respuesta de la base de datos o health check se queda en una cola, sufriendo inanición mientras esperan a que tu loop termine. La forma nativa de resolver esto es devolver manualmente el control al event loop. Esto lo haces usando un patrón específico: haciendo await de asyncio punto sleep con un argumento de cero. A primera vista, hacer un sleep de cero segundos parece una operación inútil. ¿Por qué pedirle al sistema que no espere nada de tiempo? Aquí está la clave. Un sleep de cero segundos no tiene que ver con el paso del tiempo. Es una señal explícita para el event loop. Cuando haces await de un sleep de cero, la coroutine actual se suspende inmediatamente. El event loop toma el control, coloca tu task suspendida al final de la cola de ejecución, y comprueba si hay otras tasks programadas listas para ejecutarse. Si un network handler en segundo plano está esperando para confirmar una conexión entrante, le llega su turno. Una vez que las otras tasks llegan a sus propios puntos de await o terminan, tu task original vuelve al principio de la cola y se reanuda justo donde lo dejó. Apliquemos esto a un escenario concreto. Estás escribiendo una función async para procesar millones de registros de un archivo JSON. Si ejecutas un while loop del tirón, tu servidor parecerá muerto. En su lugar, introduces una variable contador. Dentro del loop, procesas un registro e incrementas el contador. Luego, añades una condición simple. Si el contador indica que han pasado cien iteraciones, haces await de asyncio punto sleep cero. Esto divide la computación masiva en chunks manejables. El loop procesa cien registros, se hace a un lado para dejar que el servidor responda pings o acepte nuevos datos, y luego reanuda el parsing de los siguientes cien. El número de iteraciones entre yields es un parámetro que debes ajustar. Hacer yield en cada iteración añade demasiado overhead, porque suspender y reanudar una coroutine tiene un pequeño coste computacional. Hacer yield cada diez mil iteraciones aún podría bloquear el event loop durante demasiado tiempo. Cien es un punto de partida razonable para mantener al loop respirando. Forzar un sleep de cero segundos es la forma más sencilla de mantener tu aplicación cooperativa, asegurando que un único loop pesado nunca deje sin recursos al resto de tu sistema. Gracias por escuchar, ¡happy coding a todos!
8

Sincronización: Locks y Mutexes

4m 23s

Evita las condiciones de carrera en código asíncrono. Exploramos asyncio.Lock, analizamos su naturaleza no segura para hilos (non-thread-safe) y mostramos por qué los locks de threading congelarán tu event loop.

Descargar
Hola, soy Alex de DEV STORIES DOT EU. asyncio, episodio 8 de 20. Metes un lock de threading estándar en tu aplicación async para proteger un recurso compartido, y de repente todo tu event loop se congela por completo. El lock cumplió su función, pero detuvo todo lo demás en el proceso. Para solucionar esto sin bloquear el loop, utilizamos Sincronización: Locks y Mutexes. Un Lock de asyncio, a menudo llamado mutex, garantiza el acceso exclusivo a un recurso compartido entre tareas async. Primero, tenemos que aclarar una confusión común. No puedes usar un lock de thread estándar del módulo threading de Python dentro de una aplicación async. Un lock de threading opera a nivel del sistema operativo. Si no puede adquirir el lock, pausa todo el thread. Como asyncio ejecuta múltiples tareas de forma cooperativa en un solo thread, bloquear ese thread significa que el event loop se detiene. No se envían network requests, no avanzan los temporizadores. Todo se congela. Un lock de asyncio soluciona esto al ser task-safe, no thread-safe. Cuando una tarea de asyncio intenta adquirir un mutex bloqueado, no bloquea el thread. En su lugar, se suspende y cede el control de vuelta al event loop. Esto permite que otras tareas no relacionadas continúen su trabajo mientras la primera tarea espera en la cola. Vamos a anclar esto a un escenario concreto. Tienes una aplicación con docenas de tareas async haciendo llamadas a APIs externas. Tu token OAuth caduca. Dos tareas diferentes detectan el token caducado en el mismo milisegundo exacto. Sin sincronización, ambas tareas enviarán de forma independiente una request al servidor de autenticación para hacer un refresh del token. Este trabajo redundante puede activar los rate limits o invalidar inmediatamente el primer token debido a políticas de rotación estrictas. Para evitar esta race condition, creas un único lock de asyncio cuando inicializas tu aplicación. Este objeto lock se pasa o se comparte entre todas tus tareas de la API. Ahora, fíjate en el flujo. Tanto la Tarea A como la Tarea B detectan el token caducado. La Tarea A llega primero al bloque de sincronización y hace await del lock. Lo adquiere con éxito. La Tarea B llega una fracción de segundo después y hace await del mismo lock. Como la Tarea A lo tiene, la Tarea B se pone a dormir, dejando que el event loop gestione otras tareas. Cuando múltiples tareas esperan el mismo lock, asyncio las pone en cola. Una vez que se hace el release del lock, el event loop despierta a la primera tarea de la cola. La Tarea A solicita de forma segura el nuevo token, actualiza la variable compartida del token y libera el lock. En ese momento, el event loop despierta a la Tarea B. La Tarea B finalmente adquiere el lock. Sin embargo, antes de hacer una network call, la Tarea B vuelve a comprobar el token. Ve que el token ya es válido, se salta el paso de refresh, libera el lock y continúa con su request principal a la API. La forma más segura de implementar esta lógica es usando un context manager asíncrono. En tu código, escribes un statement async with seguido del objeto lock. Cuando la ejecución entra en este bloque, espera el acceso exclusivo. Cuando la ejecución sale del bloque, ya sea normalmente o porque un error hizo fallar la tarea, libera automáticamente el lock. No necesitas llamar manualmente a los métodos acquire o release, lo que elimina el riesgo de dejar un lock activado para siempre por accidente. Aquí está la clave. Un lock de asyncio no protege tu estado de otros threads del sistema operativo; protege tu estado de que tus propias tareas concurrentes se pisen entre sí mientras hacen await de otras operaciones. Gracias por pasarte. Espero que hayas aprendido algo nuevo.
9

Coordinación de estado con Events

3m 40s

Aprende a transmitir señales a múltiples tareas en espera. Explicamos cómo asyncio.Event y asyncio.Condition reemplazan de forma elegante los ineficientes bucles de consulta (polling).

Descargar
Hola, soy Alex de DEV STORIES DOT EU. asyncio, episodio 9 de 20. Tienes cincuenta tareas esperando a que se conecte una base de datos. Sin duda, no quieres que estén haciendo polling en un bucle, desperdiciando ciclos de CPU mientras comprueban si la conexión está lista. Necesitas una única señal de broadcast que les indique a todas que empiecen a lanzar queries al mismo tiempo. Esto es precisamente lo que se consigue coordinando el estado con Events y Conditions. Un Event de asyncio gestiona un sencillo flag booleano interno. Empieza en false. Antes de analizar el flujo, aclaremos una confusión común entre Events y Locks. Un Lock otorga acceso exclusivo a exactamente una tarea a la vez, impidiendo el acceso a las demás. Un Event hace lo contrario. Notifica a varias tareas en espera simultáneamente, permitiéndoles continuar a todas a la vez. Piensa en ese escenario de conexión a la base de datos. Tu background task está trabajando para establecer la conexión. Mientras tanto, tus cincuenta worker tasks llegan a un punto en el que necesitan la base de datos. Cada worker llama al método wait de tu objeto Event compartido. Como el flag interno es false, las cincuenta tareas se suspenden. Se quedan inactivas. Finalmente, la background task tiene éxito y llama al método set del Event. El flag pasa a true. Al instante, las cincuenta worker tasks suspendidas se despiertan y reanudan su ejecución. Si necesitas cerrar la conexión más tarde, puedes llamar al método clear del Event. El flag vuelve a false, y cualquier llamada futura a wait volverá a bloquearse. También puedes comprobar el estado actual del flag en cualquier momento llamando al método is set, que devuelve true o false sin bloquear la tarea. Eso cubre las señales de broadcast simples. A veces, un solo flag booleano no es suficiente. Puede que tengas varias tareas que necesiten esperar a que un recurso compartido alcance un estado complejo específico, y necesiten acceso exclusivo para comprobar o modificar ese estado de forma segura. Aquí es donde entra en juego asyncio Condition. Un Condition se basa en un Lock subyacente. Para hacer cualquier cosa con un Condition, una tarea primero debe adquirirlo. Una vez adquirido, la tarea comprueba el estado compartido. Si el estado no es el que necesita la tarea, la tarea llama al método wait del Condition. Aquí está la clave. Llamar a wait en un Condition hace dos cosas a la vez: libera el Lock subyacente, permitiendo que otras tareas accedan al estado, y suspende la tarea actual. Mientras esa tarea está suspendida, otra tarea puede adquirir el Condition, modificar el estado compartido y, a continuación, llamar al método notify. El método notify recibe un argumento que especifica exactamente cuántas tareas en espera despertar, por defecto una. También puedes llamar a notify all para despertar a todas a la vez. Cuando una tarea suspendida se despierta, no se ejecuta inmediatamente sin más. Debe esperar a volver a adquirir el Lock subyacente antes de que el método wait retorne. Dado que otra tarea podría coger el Lock y modificar el estado antes de que la tarea despertada tenga su turno, la llamada a wait casi siempre se coloca dentro de un bucle while que comprueba continuamente el estado deseado. Una vez que recupera el Lock y el estado es correcto, puede continuar de forma segura y, finalmente, liberar el Condition. Al decidir entre los dos, recuerda que un Event es un simple broadcast que informa a las tareas de que ha ocurrido una acción puntual, mientras que un Condition permite que las tareas esperen de forma segura un cambio de estado complejo sin hacer polling constantemente de un recurso bloqueado. Gracias por dedicarme unos minutos. Hasta la próxima, cuídate.
10

Limitar la concurrencia con Semaphores

4m 13s

Protege recursos frágiles y evita baneos por límite de peticiones (rate-limiting). Descubre cómo asyncio.Semaphore limita la ejecución concurrente sin bloquear tu arquitectura.

Descargar
Hola, soy Alex de DEV STORIES DOT EU. asyncio, episodio 10 de 20. Lanzar diez mil requests asíncronas a una API de terceros frágil es una forma muy eficiente de que te baneen la IP permanentemente. Tu código se ejecuta sin problemas, pero el servidor del otro lado colapsa ante el pico repentino de tráfico. Para proteger los servicios externos y tu propio acceso, tienes que hacer throttle a tu aplicación. Ese escudo es limitar la concurrencia con Semaphores. Ayuda aclarar un malentendido común desde el principio. Un Semaphore no es un rate limiter. No limita cuántas requests hace tu programa por segundo. En su lugar, limita las operaciones concurrentes. Controla estrictamente cuántas tasks pueden ejecutar un bloque específico de operaciones de red o de archivos en el mismo instante. Si una task termina su llamada a la API en diez milisegundos, ese hueco se libera inmediatamente para la siguiente task en la cola. Podrías seguir procesando cientos de operaciones por segundo, siempre y cuando no haya más del límite permitido en vuelo simultáneamente. Un Semaphore de asyncio gestiona un contador interno muy simple. Cuando creas el objeto Semaphore, le pasas un valor inicial. Pongamos el caso de limitar las requests HTTP salientes hacia una API externa delicada a exactamente diez conexiones concurrentes. Inicializas tu Semaphore con un valor de diez. Antes de que cualquier task asíncrona haga una request de red, debe hacer acquire del Semaphore. Esta acción resta uno al contador interno. Cuando la request de red termina, la task hace release del Semaphore, sumando uno de vuelta al contador. Aquí está la clave. Si diez tasks ya han hecho acquire del Semaphore, el contador se queda a cero. Cuando la undécima task intenta hacer acquire, esa task se suspende. El método acquire bloquea el progreso hasta que una de las diez primeras tasks termina y hace su release. Este simple bloqueo numérico asegura que nunca superes tu límite estricto de diez conexiones activas. En la práctica, rara vez deberías llamar a los métodos acquire y release manualmente. En su lugar, usas el Semaphore como un context manager asíncrono. Al envolver tu request HTTP en un statement with asíncrono, Python garantiza que se haga el release del Semaphore cuando se sale del bloque de código. Este release ocurre incluso si la API da un timeout, corta la conexión o lanza una exception no controlada. Si intentas hacer releases manuales y un error se salta tu llamada al release, ese hueco de concurrencia se pierde para siempre. Si pierdes los diez huecos por errores de red transitorios, todo tu programa entra en deadlock silenciosamente. Hay un peligro sutil con el Semaphore estándar. Si un error de lógica en tu código hace que una task haga release del Semaphore más veces de las que hizo acquire, el contador interno subirá por encima de tu límite original de diez. De repente, tu escudo de concurrencia se rompe y, sin darte cuenta, estás enviando doce o quince requests simultáneas. Para evitar esto, deberías usar un Bounded Semaphore de asyncio. Un Bounded Semaphore se comporta exactamente igual que un Semaphore estándar, pero rastrea el valor inicial que le diste. Si una task rebelde intenta hacer release del Semaphore por encima de ese límite inicial, el Bounded Semaphore lanza inmediatamente un value error. Falla pronto y de forma ruidosa en lugar de saturar silenciosamente la API externa. Usa siempre un Bounded Semaphore por defecto, a menos que tengas una razón arquitectónica muy específica para inflar tus límites de concurrencia dinámicamente. Los Bounded Semaphores atrapan los errores lógicos de release en el momento en que ocurren, manteniendo estrictos los límites de conexión de tu API y haciendo que tus sistemas funcionen de forma predecible. Eso es todo por este episodio. Gracias por escuchar, ¡y sigue programando!
11

Flujos de trabajo Productor-Consumidor

3m 35s

Desacopla productores rápidos de consumidores lentos de forma segura. Explora asyncio.Queue, la señalización de finalización de tareas y las nuevas mecánicas de apagado (shutdown) para colas.

Descargar
Hola, soy Alex de DEV STORIES DOT EU. asyncio, episodio 11 de 20. Tienes un servidor web async gestionando miles de requests por segundo, y por cada request, necesitas escribir una entrada de log en disco. Si tu servidor espera a que finalice esa escritura en disco antes de responder, el rendimiento se desploma. La forma más fiable de desacoplar productores rápidos de consumidores lentos en Python async está integrada directamente en la standard library. Hoy veremos workflows de productor-consumidor usando colas de asyncio. Algunos desarrolladores que vienen del multi-threading asumen que necesitan envolver esta cola en locks para evitar race conditions. No hace falta. La cola de asyncio está diseñada específicamente para tareas concurrentes que se ejecutan en un único event loop. Es inherentemente segura para esas tareas. Deja las colas thread-safe del módulo queue estándar para threading; usa la versión de asyncio para async. Piensa en la cola como un pipe. En un extremo, tienes productores haciendo push de items. En el otro extremo, tienes consumidores haciendo pull de esos items. Usemos ese escenario de logging. Tu request handler web es el productor. Recibe una request entrante, formatea un evento de log y llama al método asíncrono put en la cola. Si estableces un tamaño máximo al crear la cola, obtienes backpressure automático. Cuando la cola está llena, hacer await del método put pausa al productor hasta que se libera espacio. Esto evita que un pico de tráfico abrumador agote la memoria de tu sistema. Al otro lado del pipe, tienes una background task separada actuando como consumidor. Esta tarea se ejecuta en un loop continuo. Llama al método asíncrono get en la cola. Si la cola está vacía, el consumidor se va a dormir de forma segura. El event loop lo despierta en el momento exacto en que un productor deja caer un nuevo evento de log en el pipe. El consumidor toma el evento, lo escribe en disco y luego avisa de que ese trabajo específico está completo llamando a un método llamado task done. Gestionar este flujo durante el teardown de la aplicación es crítico. Si necesitas hacer un shutdown graceful de tu servidor web, quieres asegurarte de que todos los eventos de log encolados realmente se escriban en disco. La cola tiene un método llamado join. Cuando haces await de join, tu programa se bloquea hasta que el número de llamadas a task done coincida exactamente con el número de items que se metieron originalmente en la cola. Esto garantiza que cada trabajo se haya procesado por completo. Aquí está la clave. Python 3.13 introdujo un nuevo método de cola llamado shutdown. Anteriormente, detener un loop productor-consumidor de forma limpia requería pasar valores sentinel especiales, como inyectar un objeto None en la cola, solo para decirle al consumidor que saliera de su loop. Ahora, simplemente puedes llamar a shutdown. Cuando haces esto, cualquier tarea actualmente bloqueada esperando para hacer put o get de un item recibe inmediatamente una excepción QueueShutDown. Capturas esta excepción en tus worker tasks, limpias tus recursos y sales limpiamente sin ninguna lógica sentinel frágil. Al diseñar un sistema asyncio, recuerda que las colas no son solo estructuras de datos; son mecanismos de control de flujo que gestionan el backpressure de forma nativa, manteniendo estable tu memory footprint incluso cuando los productores superan con creces a los consumidores. Eso es todo por este episodio. Gracias por escuchar, y ¡sigue construyendo!
12

Networking de alto nivel con Streams

4m 01s

Sumérgete en los IO Streams de alto nivel. Hablamos sobre StreamReader, StreamWriter y por qué omitir await writer.drain() puede destruir silenciosamente la memoria de tu servidor.

Descargar
Hola, soy Alex de DEV STORIES DOT EU. asyncio, episodio 12 de 20. Estás enviando datos a través de una conexión de red y tu loop parece funcionar perfectamente. Pero, en segundo plano, tu aplicación está consumiendo silenciosamente gigabytes de memoria hasta que el sistema la mata. El problema suele reducirse a una línea de código que falta para gestionar el flow control. Por eso, hoy vamos a ver el networking de alto nivel con streams. Asyncio proporciona una API de alto nivel para trabajar con conexiones de red sin tocar raw sockets ni protocolos de transporte de bajo nivel. Para establecer una conexión TCP, usas una función top-level llamada open_connection. Le pasas un string para el host y un integer para el port. Inmediatamente devuelve una tuple de dos objetos: un StreamReader y un StreamWriter. Si estás montando un server en lugar de un client, usas start_server. Le pasas un callback, un host y un port. Cada vez que se conecta un nuevo client, asyncio dispara tu callback, pasándole un reader y un writer dedicados para esa conexión específica del client. El StreamReader es tu interfaz para recibir datos. Proporciona métodos asíncronos para sacar bytes de la red. Puedes leer un número máximo específico de bytes usando el método read. Si estás parseando protocolos basados en líneas, puedes leer hasta un separador específico, como un newline, usando el método readuntil. Si tu protocolo requiere un header de tamaño fijo, puedes usar readexactly, que esperará hasta que llegue ese número exacto de bytes. Como todas estas operaciones dependen del tráfico de red y la latencia, pausan la coroutine, lo que significa que debes hacerles await. Ahora, la segunda pieza de esto es el StreamWriter. Este objeto se encarga de enviar los datos hacia fuera. Usas el método write para meter bytes en el stream. Aquí está la clave. El método write es una función normal, no asíncrona. No le haces await. Cuando llamas a write, no estás poniendo datos instantáneamente en el cable de red. Simplemente estás metiendo datos en un buffer interno de asyncio. El event loop subyacente intenta hacer flush de este buffer a la red en segundo plano. Este buffer es donde los developers se meten en problemas. Piensa en un client TCP enviando un payload de archivo masivo a un server lento. Si pones tu llamada a write en un loop muy ajustado leyendo chunks de un disco local, Python leerá el archivo muchísimo más rápido de lo que la red puede transmitirlo. Como write no bloquea tu código, tu loop sigue girando. El buffer interno absorbe todo el archivo, consumiendo toda la memoria disponible del sistema. Aquí es donde entra en juego la backpressure. Para gestionar el flow control, debes emparejar tus llamadas a write con el método drain. El método drain es asíncrono, lo que significa que le haces await. Cuando le haces await a drain, le dices al event loop que pause tu coroutine si el buffer interno ha superado su high-water mark. Tu código espera hasta que el proceso en segundo plano empuje suficientes datos por la red para reducir el buffer a un tamaño seguro. La red tiene tiempo para ponerse al día, el buffer se vacía y tu uso de memoria se mantiene plano. Cuando terminas de enviar tu archivo, llamas al método close en el writer. Al igual que write, close no es una función async. Para asegurar que la conexión realmente se cierre limpiamente y que todos los bytes finales se hagan flush antes de que tu programa continúe, lo sigues haciéndole await al método wait_closed. El StreamWriter hace que escribir en una red parezca instantáneo, pero la física sigue aplicando. Siempre hazle await a drain después de hacer write para asegurar que tu aplicación respete la velocidad real de la conexión de red. ¡Gracias por escuchar, happy coding a todos!
13

Creación de servidores Async

4m 04s

Construye servidores de red altamente concurrentes. Aprende cómo asyncio.start_server abstrae las conexiones de los clientes, generando una tarea aislada para cada peer.

Descargar
Hola, soy Alex de DEV STORIES DOT EU. asyncio, episodio 13 de 20. Crear un servidor TCP altamente concurrente en Python suele implicar lidiar con thread pools o configuraciones complejas del event loop. En realidad, puedes gestionar miles de conexiones con menos de diez líneas de código. Eso es exactamente lo que vamos a ver hoy creando servidores asíncronos con streams de asyncio. La base de un servidor de red en asyncio es una función llamada start_server. Le pasas tres cosas: un callback, una dirección IP y un puerto. Cuando haces await de start_server, se vincula a esa dirección y empieza a escuchar las conexiones TCP entrantes en la interfaz de red que has especificado. Los desarrolladores suelen asumir que necesitan interceptar manualmente estas conexiones entrantes y escribir código boilerplate para enviarlas a worker threads o background tasks personalizadas. Eso es completamente innecesario. El framework gestiona la concurrencia por ti. Cada vez que un nuevo cliente se conecta a tu puerto, start_server crea automáticamente una nueva task de asyncio dedicada exclusivamente a ese cliente en concreto. Piensa en crear un servidor de chat sencillo. Cuando se conecta tu primer usuario, start_server ejecuta tu callback y le pasa dos objetos: un stream reader y un stream writer. Si se conectan cincuenta usuarios más simultáneamente, se levantan al instante cincuenta tasks independientes para ejecutar exactamente ese mismo callback. Cada task recibe su propio par aislado de reader y writer. Dentro de tu callback, escribes la lógica como si solo estuvieras hablando con una persona a la vez. Usas el objeto reader para escuchar los mensajes entrantes. Haces await del método read en el reader, especificando un número máximo de bytes que quieres aceptar, como cien bytes. El reader te da los bytes en crudo de la red, que tú decodificas en un string de texto estándar. Para responder al cliente, inviertes el proceso. Codificas tu string de respuesta de nuevo a bytes y se lo pasas directamente al objeto writer. Aquí está la clave. Pasarle datos al writer no es una operación asíncrona, pero asegurarte de que esos datos realmente salgan de la máquina física sí lo es. Después de darle los datos al writer, tienes que hacer await del método drain del writer. Hacer drain pausa tu task de cliente actual hasta que el buffer de red del sistema operativo tenga suficiente espacio libre para enviar los bytes por el cable. Este paso es crítico porque evita que tu servidor consuma toda la memoria disponible si un cliente tiene una conexión de red lenta. Cuando la conversación termina, o si el cliente se desconecta, le dices al writer que se cierre. Luego haces await del método wait_closed del writer para asegurarte de que todos los bytes finales se transmitan y el socket subyacente se cierre limpiamente. De vuelta en tu función principal de configuración, start_server devolvió un objeto server. Por defecto, el server deja de escuchar si el script principal de Python llega al final de sus instrucciones. Para mantener tu sala de chat abierta indefinidamente, coges ese objeto server y haces await de su método serve_forever. Esto bloquea la task principal de asyncio en un bucle infinito, aceptando silenciosamente nuevas conexiones y creando nuevas tasks de cliente en background. El verdadero poder de este diseño es que abstrae la complejidad de la red. Escribes código directo y secuencial para una única conexión aislada, y el event loop lo escala automáticamente a través de tasks concurrentes. Si quieres apoyar el programa, puedes buscar DevStoriesEU en Patreon. Eso es todo por este episodio. Gracias por escuchar, ¡y sigue creando!
14

Subprocesses no bloqueantes

4m 05s

Ejecuta comandos de shell de forma asíncrona. Descubre por qué usar el módulo estándar subprocess detiene el event loop, y cómo asyncio.create_subprocess_exec lo soluciona.

Descargar
Hola, soy Alex de DEV STORIES DOT EU. asyncio, episodio 14 de 20. Creas una API web asíncrona, lanzas un comando estándar del sistema dentro de un endpoint y, de repente, el resto de tareas concurrentes se paralizan al instante. Nada se mueve hasta que ese comando del sistema termina. El culpable es el módulo subprocess estándar de Python, y para solucionarlo necesitas usar subprocesos no bloqueantes. Llamar a una función como subprocess punto run ejecuta un comando del sistema operativo y espera a que termine. En una aplicación asíncrona de Python, el event loop se ejecuta en un solo hilo. Cuando bloqueas ese hilo esperando al sistema operativo, el event loop se detiene. Cualquier otra petición concurrente a tu API se queda congelada. Para solucionar esto, asyncio proporciona sus propias funciones de subprocess diseñadas específicamente para el event loop. La herramienta principal es asyncio punto create subprocess exec. Aquí está la clave. Esta función no ejecuta el comando directamente en Python. Le pide al sistema operativo que cree un proceso hijo, pero en lugar de bloquearse esperando el resultado, devuelve inmediatamente el control al event loop. Tu API gestiona otras peticiones mientras el programa externo se ejecuta. Imagina el caso de una API web que convierte archivos de vídeo usando FFmpeg. Quieres lanzar la conversión y hacer un stream de los logs de salida de vuelta al usuario en tiempo real. Dentro de tu endpoint asíncrono, llamas a create subprocess exec. Le pasas el nombre del programa, FFmpeg, seguido de sus argumentos. Para capturar los logs, le dices a la función que redirija la standard output y el standard error a pipes de asyncio. La función devuelve un objeto Process de asyncio. Este objeto representa el comando del sistema operativo en ejecución y te da hooks asíncronos para interactuar con él. Como has redirigido las salidas a pipes, el objeto Process las expone como stream readers asíncronos. Lees los logs de FFmpeg iterando sobre el stream de standard error de forma asíncrona, ya que FFmpeg normalmente guarda los logs ahí. Por cada línea que produce el proceso externo, tu loop asíncrono se despierta, lee la línea y hace un stream de vuelta al usuario web. Mientras espera la siguiente línea, el event loop de Python vuelve directamente a atender a otros usuarios. Consigues un streaming de logs en tiempo real sin congelar el servidor. Si no necesitas hacer stream de la salida línea por línea, el objeto Process también proporciona un método communicate asíncrono. Haces await de communicate para enviar datos a la standard input y leer todos los datos de la standard output y del standard error de una sola vez. Esto mantiene el loop libre hasta que el proceso externo termina por completo y devuelve los datos. Si has gestionado los streams manualmente como en el ejemplo de FFmpeg, en su lugar haces await del método wait en el objeto Process para esperar a que el proceso termine y recoger su exit code. Al event loop le da igual si el sistema operativo está haciendo el cálculo real; si tu código Python espera de forma síncrona a que el sistema operativo responda, toda tu aplicación asíncrona se queda muerta. Eso es todo por este episodio. Gracias por escuchar, ¡y sigue programando!
15

Futures: El puente de bajo nivel

4m 18s

Descubre la base de las declaraciones await. Examinamos asyncio.Future, su papel como resultado eventual y cómo sirve de puente entre el código heredado basado en callbacks y la sintaxis moderna.

Descargar
Hola, soy Alex de DEV STORIES DOT EU. asyncio, episodio 15 de 20. Escribes código asíncrono limpio y moderno, pero tarde o temprano tienes que interactuar con una librería antigua y obstinada que depende completamente de callbacks. No puedes hacer await a un callback directamente, lo que rompe todo tu flujo asíncrono. El mecanismo que une estos dos mundos son los Futures: el puente de bajo nivel. Aclaremos de inmediato una confusión común. La gente suele confundir Tasks con Futures. Un Task es una subclase específica de un Future. Un Task envuelve una coroutine y la programa activamente en el event loop, dirigiendo su ejecución paso a paso. Un Future no ejecuta nada. No tiene lógica de ejecución propia. Es simplemente un contenedor de estado. Es una primitiva de bajo nivel que representa el resultado final de una operación asíncrona. Cuando escribes Python moderno, casi nunca instancias un Future directamente. El event loop los crea por debajo. Pero cuando necesitas envolver código legacy basado en callbacks, los construyes manualmente. Imagina un escenario en el que estás usando una librería de protocolo de red antigua. Tiene un método request que recibe una dirección de red, un callback de éxito y un callback de error. Quieres que tu función async moderna simplemente haga await en esta request. Así es como cierras la brecha. Dentro de tu función async, obtienes el event loop actual en ejecución y le pides que cree un nuevo objeto Future. En este preciso instante, el Future se encuentra en estado pending. Está vacío y esperando. A continuación, escribes una pequeña función callback de éxito. Cuando se activa, esta función recibe los datos entrantes y llama al método set result en tu Future. También escribes un callback de error que llama al método set exception en el mismo Future. Pasas ambas funciones al método request legacy y arrancas la llamada de red. Finalmente, haces await al Future. Aquí está la clave. Hacer await a un Future en estado pending pausa la coroutine actual. Devuelve el control al event loop, permitiendo que se ejecuten otros tasks. Tu código se queda congelado en esa instrucción await. Mientras tanto, el cliente legacy hace su input y output de red en segundo plano. Cuando llegan los datos, el cliente legacy activa tu callback de éxito. Tu callback llama a set result en el Future. El Future pasa inmediatamente del estado pending al estado finished. El event loop detecta este cambio de estado. Despierta a la coroutine que estaba esperando ese Future, desempaqueta el resultado almacenado, y tu función async reanuda su ejecución justo como si hubiera hecho await a una coroutine nativa. Si la llamada de red falla, tu callback de error establece una excepción en el Future en su lugar. Cuando el event loop despierta a la coroutine, lanza esa misma excepción en la línea del await. Un Future tiene reglas estrictas sobre su estado. Solo puede salir del estado pending una vez. Si un callback intenta llamar a set result en un Future que ya está finished, Python lanza un Invalid State Error. También puedes cancelar un Future manualmente. Si lo haces, entra en un estado cancelled, y cualquier coroutine que le esté haciendo await recibe inmediatamente un asyncio Cancelled Error. Los Futures proporcionan el pegamento estructural necesario entre los callbacks basados en eventos y las instrucciones await de apariencia procedimental. Entender que cada instrucción await en última instancia pausa la ejecución hasta que un Future de bajo nivel se marca como finished, te da total claridad sobre cómo opera realmente el Python asíncrono por debajo. Eso es todo por este episodio. ¡Gracias por escuchar, y sigue programando!
16

Transports y Protocols

4m 32s

Echa un vistazo bajo el capó para ver cómo asyncio se comunica con el sistema operativo. Entiende la relación 1:1 basada en callbacks entre los Transports (cómo se mueven los bytes) y los Protocols (qué significan los bytes).

Descargar
Hola, soy Alex de DEV STORIES DOT EU. asyncio, episodio 16 de 20. Cuando usas streams de asyncio de alto nivel, tu código se ve limpio, secuencial y con awaits seguros. Pero debajo de esas amigables coroutines se encuentra un motor altamente optimizado, basado en callbacks, que maneja llamadas al sistema operativo bastante liosas. Para entender cómo tu aplicación de Python se comunica realmente con una red, necesitas analizar los Transports y los Protocols. Estas dos abstracciones forman la base del networking de asyncio. Siempre trabajan en pareja. El Protocol maneja la lógica de la aplicación, decidiendo qué bytes enviar y cómo interpretar los datos entrantes. El Transport se encarga de la mecánica. No le importa qué significan tus datos ni cómo están formateados. Su única función es averiguar cómo empujar esos bytes por la red. Hoy, nos centraremos de lleno en la capa de transporte. Piensa en lo que sucede cuando escribes directamente en un socket TCP non-blocking. Tienes que preguntarle al sistema operativo si el socket está listo. Tienes que gestionar las escrituras parciales si el buffer de red está lleno. Tienes que controlar qué bytes se han enviado realmente y cuáles hay que volver a intentar enviar más tarde. Un Transport de asyncio oculta toda esta complejidad. Actúa como un wrapper opaco alrededor del raw socket y las llamadas subyacentes al sistema operativo. Por lo general, nunca instancias un Transport tú mismo. En su lugar, llamas a un método del event loop para crear una conexión de red. El event loop configura el socket, crea el Transport, lo vincula con tu Protocol y te devuelve la pareja. Aquí está la clave. Una vez establecida esa conexión, el Transport se encarga del buffering de entrada y salida. Cuando tu Protocol quiere enviar un mensaje, simplemente le pasa un chunk de bytes al método write del Transport. El Transport no bloquea tu código para esperar a la red. Inmediatamente mete esos bytes en su propio buffer interno. Luego, el Transport trabaja con el event loop en segundo plano, lanzando las llamadas al socket non-blocking hacia el sistema operativo. Si el sistema solo puede aceptar la mitad de los bytes en este momento, el Transport se guarda el resto y lo vuelve a intentar en la siguiente iteración del loop. Tu aplicación nunca tiene que microgestionar esa cola. El control de flujo está integrado directamente en este mecanismo. Si escribes datos más rápido de lo que la red puede enviarlos, el buffer interno del Transport empezará a llenarse. Una vez que alcanza un límite designado, el Transport dispara un callback específico en tu Protocol para pausar la escritura. Cuando el buffer por fin se vacía, dispara otro callback para reanudarla. En el lado de la recepción, el Transport escucha al event loop. Cuando el sistema operativo avisa de que han llegado bytes entrantes, el Transport los saca del socket y se los pasa directamente al Protocol a través de un callback. Todo en este bajo nivel está puramente basado en callbacks. Aquí no hay awaitables. Los Transports también proporcionan métodos estandarizados para gestionar el ciclo de vida de la conexión. Puedes cerrar un Transport de forma elegante, lo que le indica que termine de enviar cualquier dato en el buffer antes de cerrar el socket de forma segura. Si las cosas van mal, puedes llamar a un método abort para tumbar la conexión inmediatamente, descartando lo que quede en la cola. Y si tu Protocol necesita saber con quién está hablando, el Transport proporciona un método para pedir información extra, permitiéndote asomarte a través de la abstracción y recuperar la dirección IP del socket subyacente o los detalles del peer. La abstracción del Transport es lo que permite que tu código de asyncio se mantenga puramente centrado en la lógica de datos. Los Transports aíslan tu aplicación de la caótica mecánica del I/O non-blocking; cogen los raw bytes de tu Protocol y manejan silenciosamente el buffering, los reintentos y las llamadas al socket del sistema operativo necesarias para moverlos por la red. Eso es todo por este episodio. ¡Gracias por escuchar, y sigue programando!
17

Threading en un mundo Async

3m 39s

Conecta los mundos síncrono y asíncrono. Aprende a delegar código bloqueante pesado de forma segura usando executors y callbacks thread-safe sin bloquear el loop.

Descargar
Hola, soy Alex de DEV STORIES DOT EU. asyncio, episodio 17 de 20. Metes un background thread estándar en tu web server async para gestionar una tarea lenta, y de repente tu aplicación empieza a hacer deadlock o a lanzar errores de estado crípticos. Mezclar threads estándar con un event loop async es una receta para el desastre a menos que uses los puentes thread-safe designados. Hoy hablaremos de Threading en un mundo async. La regla principal de asyncio es que el event loop se ejecuta en un solo thread. Por ello, casi todos los objetos de asyncio no son thread-safe. Un error común es lanzar un background thread estándar, hacer algo de trabajo y luego intentar resolver un future async o programar un callback directamente desde ese thread. Si tocas un objeto de asyncio desde un thread distinto al que ejecuta el event loop, corromperás el estado del loop. Para enviar un mensaje desde un background thread a tu event loop, debes usar call soon threadsafe. Este es un método del propio loop. Le pasas el callback que quieres ejecutar y los argumentos. En lugar de ejecutarlo inmediatamente, tu background thread mete ese callback en una queue interna segura. El event loop principal comprueba esta queue y ejecuta tu callback de forma segura en el main thread durante su ciclo normal. Esta es la única forma segura de que un thread externo interactúe con el event loop. Ahora considera la situación inversa. Estás ejecutando tu event loop async y necesitas ejecutar un trozo de código síncrono y bloqueante. Un escenario clásico es hacer una consulta a un driver de PostgreSQL lento y síncrono como psycopg2. Si ejecutas una query de base de datos de cinco segundos directamente dentro de tu request handler async, todo tu web server se detiene. El event loop no puede procesar ningún otro tráfico de red ni timers hasta que esa query de base de datos termine. Aquí está la clave. Para evitar que el loop se congele, sacas ese trabajo bloqueante a un thread separado usando run in executor. Este es otro método del event loop. Le pasas un thread pool executor y tu función de base de datos síncrona. El loop le pasa la función a un background thread del pool y devuelve inmediatamente un objeto awaitable. Haces await de ese objeto. Mientras tu query de base de datos se ejecuta en el background thread, tu event loop es totalmente libre de pausar esa task específica e ir a gestionar cientos de otras peticiones web. Una vez que el driver de PostgreSQL por fin devuelve los datos, el thread pool pasa el resultado de forma segura de vuelta al event loop. Tu awaitable se resuelve, y tu función async original reanuda su ejecución justo donde la dejó, ahora con los resultados de la base de datos. Tienes dos puentes unidireccionales. Usa call soon threadsafe para enviar eventos desde un worker thread a tu loop async. Usa run in executor para sacar trabajo síncrono bloqueante de tu loop async a un worker thread. Nunca dejes que una llamada síncrona secuestre tu event loop, y nunca dejes que un background thread toque tus objetos async directamente. Eso es todo por este episodio. ¡Gracias por escuchar y sigue desarrollando!
18

Generadores Async y limpieza

4m 06s

Evita fugas de recursos con los generadores async. Exploramos por qué la iteración 'async for' puede dejar conexiones colgadas cuando se interrumpe, y cómo aclosing() proporciona seguridad.

Descargar
Hola, soy Alex de DEV STORIES DOT EU. asyncio, episodio 18 de 20. Te salta un timeout al obtener filas de una base de datos. Tu código maneja la exception y sigue adelante, pero días después tu aplicación crashea porque tu connection pool está completamente agotado. Saliste de un async loop antes de tiempo, y silenciosamente dejó conexiones a la base de datos abiertas en segundo plano. La solución está en dominar los async generators y el cleanup. Cuando escribes un async generator para hacer yield de elementos a lo largo del tiempo, a menudo gestionas recursos. Piensa en un cursor de base de datos. Escribes un generator que adquiere una conexión, hace yield de las filas una a una, y usa un bloque try-finally para devolver esa conexión al pool cuando termina el fetch. Si iteras por cada una de las filas, el generator termina, llega al bloque finally y hace el cleanup. Todo funciona. El peligro aparece cuando no consumes el generator entero. Si tu iteración está envuelta en un timeout, o si simplemente llegas a un break después de encontrar la fila que necesitas, el generator se pausa. Se queda suspendido en el último yield. No ha llegado al bloque finally. Tu conexión a la base de datos sigue abierta. Podrías esperar que el garbage collector de Python acabe encargándose de esto. En código síncrono, cuando un generator pierde todas las referencias y es recogido por el garbage collector, Python inyecta una exception de salida que ejecuta los bloques finally. Pero el código asíncrono complica esto. El garbage collection es un proceso síncrono. Cuando el garbage collector por fin encuentra tu async generator suspendido, no puede ejecutar de forma fiable el código de teardown asíncrono. El event loop podría estar ocupado, o incluso cerrado. Depender del garbage collector para hacer el cleanup de un async generator da como resultado un comportamiento impredecible y dangling resources. Esta es la parte importante. La documentación oficial de asyncio dice explícitamente que nunca debes depender del garbage collection para el cleanup de los async generators. Tienes que cerrarlos manualmente. La standard library proporciona una herramienta directa para esto llamada aclosing, que se encuentra en el módulo contextlib. Actúa como un async context manager. Su único trabajo es garantizar que el método aclose del generator sea llamado y se le haga await en el momento en que termines con él. En lugar de pasar tu generator directamente a un async for loop, lo envuelves. Primero creas la instancia del generator. Luego se la pasas a un statement async with aclosing. Dentro de ese bloque de contexto, ejecutas tu async for loop. Cuando estructuras tu código de esta manera, una salida temprana activa el context manager. Si un timeout interrumpe el loop, el bloque async with captura la salida. Hace await explícitamente del método aclose en el generator. Esto inyecta de forma segura la exception de salida en el generator suspendido mientras todavía te estás ejecutando activamente en el event loop. Tu bloque finally se ejecuta inmediatamente, haciendo await de cualquier paso de teardown necesario, y tu conexión a la base de datos vuelve de forma segura al pool. Siempre que un async generator adquiera conexiones de red, file descriptors o locks de base de datos, envuélvelo en aclosing antes de iterar para garantizar un cleanup determinista, independientemente de los timeouts o los early breaks. Eso es todo por este episodio. Gracias por escuchar, ¡y sigue construyendo!
19

Dominando el Debug Mode

4m 08s

Atrapa errores de concurrencia al instante. Aprende a usar PYTHONASYNCIODEBUG para perfilar callbacks lentos, descubrir coroutines sin await y localizar excepciones nunca recuperadas.

Descargar
Hola, soy Alex de DEV STORIES DOT EU. asyncio, episodio 19 de 20. Tu servidor de producción experimenta misteriosos picos de lag, y tus operaciones en background se tragan errores aleatoriamente sin dejar rastro. El problema no es la lógica de tu aplicación, sino cómo el asyncio estándar oculta los errores de concurrencia para ahorrar en performance. Dominar el Debug Mode es la respuesta para exponer estos fallos al instante. El Debug Mode de asyncio actúa como un strict mode para el event loop. Por defecto, asyncio prioriza la velocidad bruta sobre las comprobaciones de seguridad en runtime. Esto significa que, cuando las cosas van mal, suelen fallar silenciosamente. Puedes activar el Debug Mode globalmente configurando la variable de entorno PYTHONASYNCIODEBUG a uno, o ejecutando Python con el flag guion X dev. También puedes activarlo dinámicamente llamando a set debug true directamente en el objeto del event loop. Tomemos el escenario del pico de lag. Tienes un servidor web gestionando miles de requests concurrentes y, de repente, un único endpoint provoca que toda la aplicación se congele. Sospechas que una operación regex rebelde está bloqueando el thread, pero el logging estándar solo te dice cuándo empieza o termina una request, no qué bloqueó el loop entre medias. Cuando el Debug Mode está activo, el event loop mide el tiempo de ejecución de cada callback. Si un callback bloquea el loop durante más de cien milisegundos, asyncio loguea automáticamente un warning. Este warning incluye el archivo y el número de línea exactos donde ocurrió el bloqueo, apuntando directamente a esa costosa búsqueda regex. Ese umbral de cien milisegundos viene por defecto, pero puedes ajustarlo a tus requisitos específicos de latencia modificando la propiedad slow callback duration en el loop. El Debug Mode también detecta fallos de ejecución silenciosos. Un error frecuente en el código async es llamar a una función coroutine pero olvidarse de la keyword await. La función devuelve un objeto coroutine, pero la lógica real nunca se ejecuta. En la ejecución normal, ese objeto se descarta silenciosamente. El Debug Mode rastrea esto. Cuando el garbage collector limpia una coroutine sin await, el debug loop la intercepta y emite un resource warning, mostrando exactamente dónde se creó la coroutine huérfana para que puedas arreglar la invocación. Esta misma red de seguridad se aplica a las background tasks. Si una task de asyncio crashea, la excepción se guarda dentro del propio objeto task. Si tu código nunca hace un await explícito de esa task ni recupera su resultado, la excepción simplemente desaparece. Con el Debug Mode activado, asyncio monitoriza el ciclo de vida de cada task. Si una task se destruye y su excepción interna nunca se recuperó, el event loop loguea el error de forma ruidosa junto con el traceback que muestra dónde se creó originalmente la task. Estas comprobaciones añaden overhead, así que normalmente dejas el Debug Mode apagado en entornos de producción normales, reservándolo para desarrollo local o troubleshooting específico. Aquí está la clave. Activar el Debug Mode traslada la carga de encontrar bugs de concurrencia silenciosos desde tu propio logging manual directamente al propio event loop. Si disfrutas del podcast y quieres apoyarnos, busca DevStoriesEU en Patreon. Eso es todo por este episodio. Gracias por escuchar, y ¡sigue construyendo!
20

Extensión y Loops personalizados

4m 12s

El gran final. Exploramos la integración avanzada y lo que se necesita para escribir un event loop personalizado o crear una subclase de BaseEventLoop para entornos especializados de alto rendimiento.

Descargar
Hola, soy Alex de DEV STORIES DOT EU. asyncio, episodio 20 de 20. Te has chocado contra un muro de rendimiento con tu código asíncrono, y el profiling apunta directamente al propio event loop principal. No puedes reescribir la standard library, pero necesitas un control de más bajo nivel sobre cómo el sistema gestiona exactamente los sockets y las tasks. La respuesta está en extender y crear custom loops. El event loop estándar de asyncio no es una caja negra hardcodeada. Es una interfaz extensible. Fue diseñado desde cero para ser totalmente reemplazable por librerías de C de alto rendimiento o implementaciones especializadas de Python. La mayoría de los desarrolladores de aplicaciones nunca necesitarán crear un custom loop. Sin embargo, si eres el autor de un framework o estás creando un loop optimizado como uvloop, necesitas hacer bypass del comportamiento estándar e integrarte directamente con las primitivas de más bajo nivel del sistema operativo. Para crear un custom event loop, empiezas creando una subclase de BaseEventLoop. Esta clase base define todo el contrato sobre cómo deben comportarse las operaciones asíncronas. Al heredar de ella, obtienes la estructura, pero puedes hacer override de métodos específicos para interceptar y redefinir operaciones fundamentales. Piensa en la creación de sockets. En una aplicación estándar, le pides a asyncio que abra una conexión, y utiliza la implementación de sockets por defecto de Python. Pero en una subclase de custom loop, puedes hacer override de los métodos de creación de red. Esto significa que cuando la aplicación solicita una conexión de red, tu custom loop intercepta esa llamada. Entonces puedes enrutar esa petición a través de código C altamente optimizado, o vincularla directamente a características avanzadas del kernel que el Python estándar no expone. El código de la aplicación no cambia, pero la maquinaria subyacente es completamente tuya. Este control granular también se aplica a la gestión de tasks. Aquí está la clave. El event loop es el responsable de hacer un seguimiento de cada task asíncrona. Si miras bajo el capó de BaseEventLoop, encontrarás un método interno llamado underscore register task. Al hacer override de este método específico, tu custom loop intercepta una task en el microsegundo exacto en que se crea. ¿Por qué importa esto? Si estás construyendo un custom runtime, puede que necesites trackear diagnósticos profundos, implementar memory pooling especializado para las tasks, o enviar el estado de la task directamente a un servicio de monitorización personalizado. Hacer override de underscore register task te da un hook garantizado en el ciclo de vida de cada corrutina incluso antes de que empiece a ejecutarse. También puedes hacer override del método unregister correspondiente para gestionar la limpieza exactamente como lo requiera tu framework. Una vez construida tu clase de custom loop, tienes que decirle a Python que la use realmente. Esto lo haces creando una custom event loop policy. La policy es simplemente una factory que dicta qué implementación del loop se crea cuando un thread pide una. Configuras tu custom policy globalmente. A partir de ese momento, a cualquier función de la standard library que solicite un event loop se le entregará tu versión custom optimizada. El verdadero poder de asyncio no es solo la sintaxis async y await. Es el hecho de que todo el motor de ejecución es una interfaz pluggable, lista para ser intercambiada en el momento en que el rendimiento estándar limite tu arquitectura. Como esto concluye nuestra serie, te animo a leer la documentación oficial, a probar a extender estos componentes de forma práctica, o a visitar devstories dot eu para sugerir temas para futuras series. Eso es todo por este episodio. ¡Gracias por escuchar, y sigue construyendo!