Il y a Python,
et Python asynchrone

Martin Kirchgessner

22 mai 2025

👋 Bonsoir

Martin Kirchgessner

Pythoniste depuis la v2.4, à temps plein depuis 8 ans.

J'ai bossé avec Python asynchrone sur :

🤖 C'est l'histoire d'un bot

(né en 2008)

    import asyncore
    import asynchat
    class Bot(asynchat.async_chat):
        ...
        def handle_connect(self):
            ...
        def collect_incoming_data(self, data):
            ...
        def found_terminator(self):
            ...
        ...
    bot = Bot("server.irc.net", 6667)
    asyncore.loop()

💣 Quand soudain

$ python3.12 irc.py
Traceback (most recent call last):
File "/home/martin/irc.py", line 15, in <module>
    import asyncore
    ModuleNotFoundError: No module named 'asyncore'

🚀 En 2025, on async/await

Qu'est-ce qui pourrait mal se passer ?

  • async/await presque partout
  • requests → httpx
  • les tests aussi

📽️ Du coup j'ai fait cette présentation

  1. Principe et histoire de l'async
  2. async def monde_nouveau()
  3. await moralité()

📈 Questions de perf

The C10K problem , en 1999

You can buy a 1000MHz machine with 2 GB of RAM and a GB Ethernet card [...]
at 20000 clients, that's 50KHz, 100Kbytes, and 50Kbits/sec per client.

ça devrait suffire pour une réponse de 1-5Ko

et donc: NGinx, NodeJS

➰ Boucle d'évènement


        while True:
            e = eventQueue.dequeue()
            processEvent(e)
                

eventQueue remplie "par l'extérieur"

  • Messages réseau
  • IHM (clics, clavier, ...)

Python is super()

"il peut tout faire"

Même des boucles d'évènements.

🧙‍♂️ C'est déjà vieux

Quand Version Quoi
Avant 2.x gevent, Tornado, Twisted, asyncore
2012 3.3 PEP3156 module asyncio
2015 3.5 PEP492 async/await
2018 ASGI

Exemple

import asyncio

async def on_connect(reader, writer):
    line = await reader.readline()
    writer.write(line)
    await writer.drain()

async def main(host, port):
    srv = await asyncio.start_server(on_connect, host, port)
    await srv.serve_forever()

asyncio.run(main('127.0.0.1', 8888))
                

Applis Web : ✨ASGI✨

  • fait pour l'asynchrone
  • permet les websockets

(peut inclure votre app WSGI)

Plusieurs choses en même temps ?

Concurrence, synchro, verrous, ...

async def monde_nouveau()

⚙️ __main__.py

asyncio.run(main) plutôt que main()

Ou bien...
loop.run_until_complete(main),
loop.call_soon(main),
loop.run_forever(main)

... utilisez un framework !


async def ma_fonction():
    x = 1
    y = await service.envoi(x)
    return y
                

y = await ma_fonction()
                

coroutine = ma_fonction() # ⚠️
                

coroutine ?

Aparté: typage

Coroutine[YieldType, SendType, ReturnType]
async def ma_fonction() -> str:
    return "restons simples"

y = await ma_fonction()

🚦 await est prioritaire

if await fut() + 1:
    ...

pair = await fut(), 'spam'

await foo()['spam'].baz()()
if (await fut()) + 1:
    ...

pair = (await fut()), 'spam'

await ( foo()['spam'].baz()() )

Utilisations typiques

  1. Tâches de fond
  2. Websockets

⏳ Tâches de fond

def order(request):
    order = db.save_order(request)
    order.init_packaging()
    order.email_confirmation()
    return templates.confirm(order)
async def order(request):
    order = await db.save_order(request)
    app.add_background_task(order.init_packaging())
    app.add_background_task(order.email_confirmation())
    return templates.confirm(order)

📨 WebSockets

from quart import websocket

@app.websocket('/echo')
async def echo_ws():
    while True:
        data = await websocket.receive()
        await websocket.send(data)

🪂 try/finally

async def cancel_me():
    print('cancel_me(): before sleep')

    try:
        # Wait for 1 hour
        await asyncio.sleep(3600)
    except asyncio.CancelledError:
        print('cancel_me(): cancel sleep')
        raise
    finally:
        print('cancel_me(): after sleep')

🏭 Async Generator

async def iter_stream():
    async for item in OnlineCollection():
        item.truc()
class OnlineCollection:
    async def __anext__(self):
        if data := await self.fetch_data():
            return data
        else:
            raise StopAsyncIteration()

🗜️ Async Contexts

async def wait_for_context():
    async with OnlineContext() as contexte:
        contexte.truc()
class OnlineContext:
    async def __aenter__(self):
        await distant_logger.ping()

    async def __aexit__(self, exc_type, exc, tb):
        if exc:
            await distant_logger.oops(exc)
        else:
            await distant_logger.pong()
                

🪤 Conflits d'except

Evitez de yield sous un with

PEP 789 pourrait introduire with sys.prevent_yields(reason) dans Python 3.14

🐛 Asyncio: mode debug

PYTHONASYNCIODEBUG=1 python mon_appli.py
# OU
asyncio.run(..., debug=True)
# OU
loop.set_debug()

Essentiel pour voir:

  • Les oublis d'await
  • Les tâches trop longues

🔍 Voir les coroutines en "live"

pip install aiomonitor
async def wrapped_main():
    loop = asyncio.get_running_loop()
    with aiomonitor.start_monitor(loop):
        await real_main()

🧵 Il reste les threads

loop.run_in_executor(fonction_synchrone)

Il reste le GIL, aussi.

⚠️ asyncio n'est pas thread-safe ⚠️

📥 Await quoi ?

⚠️ attendre le réseau, pas le disque ⚠️

Par exemple aiosqlite fait tout dans un thread.

ℹ️ Passez par un framework

FastAPI/Quart
+ uvicorn/hypercorn

pytest_asyncio c'est rock'n'roll

await moralité()

Benchmark 1

Petite appli Quart/SQLite

~80 req/s dans 200Mb de RAM

+ un upload de 2GB de même temps:
~70 req/s, dans 200Mb de RAM

Benchmark 2

Télécharger 12000 petits objets depuis S3

session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
    pool_connections=200, pool_maxsize=200
)
session.mount("https://", adapter)

executor = concurrent.futures.ThreadPoolExecutor(max_workers=200)
    for _ in executor.map(session.get, urls_generator()):
        pass 
720 req/s, 240MB RAM

Benchmark 2

Télécharger 12000 petits objets depuis S3

tasks = []
for url in urls_generator():
    tasks.append(asyncio.create_task(aiohttp.get(url)))

asyncio.gather(tasks)
620 req/s, 380MB RAM 📉

Benchmark 2

Les deux implem ne sont pas à 100% de CPU 🤔

Pas si vite !

Mais, résistant à la charge

Vous commencez une appli ?

  • 🪐 Web ? go async !
  • 🖱️ GUI ? go async !
  • 📂 data ? ...

Vous avez déjà une appli ... 🤷

🪦 Souvenirs de 2 → 3

  • C'est pas compliqué
  • Mais modifie tout
  • Remet en question vos dépendances

🧽 Changements de librairies

  • Frameworks Web: WSGI → ASGI
    • Flask → Quart
    • Django → DjangoChannels
    • ... → FastAPI
  • requests → httpx/aiohttp
  • etc.

Un monde à part

Vous vous souvenez du bot du début ?

L'équivalent "moderne" de asynchat sont les Transport/Protocol

Une appli WSGI ça peut suffire aussi.

Et vous, ça s'est bien passé ?

Merci d'avoir suivi ! Les slides sont sur https://mkir.ch

PEPs !

  • PEP 492 Coroutines with async and await syntax
  • PEP 525 Asynchronous Generators
  • PEP 530 Asynchronous Comprehensions
  • PEP 3156 the “asyncio” Module
  • PEP 789 Preventing task-cancellation bugs by limiting yield in async generators