
asyncio est le module standard de Python pour la programmation asynchrone : il permet à un programme de faire autre chose pendant qu'il attend une opération lente. Quand votre code passe son temps à attendre le réseau, une API ou une base de données, asyncio lance des centaines de ces attentes en même temps, sur un seul thread, au lieu de les enchaîner. Ce guide couvre les mots-clés async/await, le lancement avec asyncio.run, l'exécution concurrente avec gather et TaskGroup, et surtout quand l'asynchrone est le bon outil.
Il s'adresse aux développeurs déjà à l'aise avec les fonctions qui veulent accélérer du code orienté entrées-sorties, ou comprendre le async/await qu'ils croisent dans des outils comme LiteLLM, les serveurs MCP ou les frameworks web. Tous les exemples ont été exécutés avec Python 3.12.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Comprendre ce que résout l'asynchrone (et ce qu'il ne résout pas).
- Écrire une coroutine avec
async defet l'attendre avecawait. - Lancer du code asynchrone avec
asyncio.run. - Exécuter des tâches en concurrence avec
gatheretTaskGroup. - Savoir quand choisir asyncio plutôt que du synchrone ou des threads.
Le problème : attendre bloque tout
Section intitulée « Le problème : attendre bloque tout »Prenons un programme qui télécharge trois ressources, chacune prenant une seconde. En code synchrone classique, on attend la première, puis la deuxième, puis la troisième : trois secondes au total, alors que le processeur ne fait rien pendant ces attentes.
C'est le gaspillage que résout l'asynchrone : pendant qu'une tâche attend le réseau, le programme démarre les autres. Les trois attentes se chevauchent, et le total tombe à une seconde. L'asynchrone ne calcule pas plus vite : il attend plus intelligemment.
Coroutines : async def et await
Section intitulée « Coroutines : async def et await »Une fonction déclarée avec async def n'est pas une fonction ordinaire : c'est une coroutine. L'appeler ne l'exécute pas, elle renvoie un objet qu'il faut attendre avec await.
import asyncio
async def telecharger(nom): print(f"début {nom}") await asyncio.sleep(1) # simule une E/S lente, sans bloquer print(f"fini {nom}") return f"{nom} ok"Le point clé est await : il marque l'endroit où la coroutine peut rendre la main pendant qu'elle attend. Ici, await asyncio.sleep(1) simule une opération réseau d'une seconde. Pendant cette attente, la boucle d'événements peut faire progresser d'autres coroutines.
Lancer du code asynchrone : asyncio.run
Section intitulée « Lancer du code asynchrone : asyncio.run »Une coroutine ne s'exécute pas toute seule : il faut une boucle d'événements (event loop), le moteur qui orchestre les coroutines. Le point d'entrée est asyncio.run(), appelé une seule fois tout en haut du programme.
async def principal(): resultat = await telecharger("A") print(resultat)
asyncio.run(principal()) # démarre la boucle, exécute, puis la fermeasyncio.run démarre la boucle, exécute la coroutine jusqu'au bout, puis referme proprement la boucle. À l'intérieur des coroutines, on enchaîne ensuite les appels avec await, sans jamais rappeler asyncio.run.
Séquentiel ou concurrent : le vrai gain
Section intitulée « Séquentiel ou concurrent : le vrai gain »Attendre les coroutines une par une ne gagne rien : elles s'exécutent en série.
async def sequentiel(): await telecharger("A") # 1 s await telecharger("B") # 1 s await telecharger("C") # 1 s# total : 3 secondesPour les lancer en même temps, on utilise asyncio.gather, qui démarre toutes les coroutines et attend l'ensemble de leurs résultats.
async def concurrent(): resultats = await asyncio.gather( telecharger("A"), telecharger("B"), telecharger("C"), ) return resultats# total : 1 secondeLa différence est spectaculaire et mesurable : le passage de sequentiel() à concurrent() fait tomber le temps total de 3 secondes à 1 seconde, car les trois attentes se chevauchent. Plus vous avez d'appels à faire, plus le gain est grand.
TaskGroup : la concurrence structurée (Python 3.11)
Section intitulée « TaskGroup : la concurrence structurée (Python 3.11) »Depuis Python 3.11, la façon recommandée de lancer plusieurs tâches est asyncio.TaskGroup, un gestionnaire de contexte qui attend automatiquement que toutes les tâches soient finies à la sortie du bloc.
async def principal(): async with asyncio.TaskGroup() as tg: tg.create_task(telecharger("A")) tg.create_task(telecharger("B")) # à la sortie du bloc, les deux tâches sont terminéesSon avantage décisif sur gather : si une tâche échoue, toutes les autres sont annulées automatiquement et l'erreur est propagée proprement. C'est le principe de la concurrence structurée, plus sûr pour la gestion des erreurs dans du code réel.
Poser une limite de temps avec timeout
Section intitulée « Poser une limite de temps avec timeout »Toujours depuis Python 3.11, asyncio.timeout annule une opération qui prend trop de temps, indispensable face à un service réseau qui ne répond pas.
async def principal(): try: async with asyncio.timeout(0.5): await telecharger("lent") # prend 1 s : sera annulé except TimeoutError: print("tâche trop lente, annulée")Quand utiliser asyncio (et quand ne pas)
Section intitulée « Quand utiliser asyncio (et quand ne pas) »C'est le point le plus mal compris de l'asynchrone. asyncio n'est pas une baguette magique de performance : son intérêt dépend de ce qui limite votre programme.
| Votre programme est limité par... | Bon outil |
|---|---|
| des entrées-sorties (réseau, API, base de données) | asyncio |
| un calcul intensif (CPU : traitement d'images, maths) | multiprocessing |
| un script simple et séquentiel | le synchrone classique |
asyncio brille quand le programme passe son temps à attendre : des milliers d'appels d'API, un scraper, un serveur web. Il n'accélère pas un calcul qui sature le processeur, car tout tourne sur un seul thread.
À retenir
Section intitulée « À retenir »- asyncio sert à attendre efficacement des entrées-sorties, pas à calculer plus vite.
- Une fonction
async defest une coroutine : l'appeler ne l'exécute pas, il faut l'await. awaitmarque un point où la coroutine peut rendre la main pendant qu'elle attend.- On démarre tout avec
asyncio.run(), appelé une seule fois au niveau supérieur. gatheretasyncio.TaskGroup(3.11) exécutent des tâches en concurrence : 3 attentes d'1 s prennent 1 s.TaskGroupannule les autres tâches si l'une échoue (concurrence structurée) ;asyncio.timeout(3.11) borne la durée.- asyncio pour l'I/O, multiprocessing pour le CPU, synchrone pour un script simple.
- Ne jamais mettre un appel bloquant dans une coroutine : cela gèle la boucle.
FAQ : asyncio et async/await
Section intitulée « FAQ : asyncio et async/await »import asyncio
async def principal():
await asyncio.sleep(1) # attend sans bloquer
print("fini")
asyncio.run(principal())
C'est idéal pour le code orienté entrées-sorties (appels d'API en masse, scrapers, serveurs).async def définit une coroutine, pas une fonction ordinaire :async def saluer():
return "bonjour"
saluer() # ne s'exécute PAS : renvoie <coroutine>
await saluer() # exécute réellement (dans une coroutine)
- Une fonction normale s'exécute immédiatement à l'appel.
- Une coroutine doit être attendue avec
await, ou lancée avecasyncio.runau niveau supérieur.
await marque un point où la coroutine peut rendre la main pendant qu'elle attend.asyncio.run() :import asyncio
async def principal():
await tache_1()
await tache_2()
asyncio.run(principal()) # démarre et ferme la boucle d'événements
asyncio.run démarre la boucle d'événements, exécute la coroutine jusqu'au bout, puis referme la boucle. On l'appelle une seule fois, tout en haut du programme. À l'intérieur des coroutines, on enchaîne avec await, sans rappeler asyncio.run.asyncio.gather, on lance plusieurs coroutines en même temps :resultats = await asyncio.gather(
telecharger("A"), telecharger("B"), telecharger("C")
)
Trois tâches d'une seconde prennent alors une seconde au total, pas trois. À l'inverse, await une par une les exécute en série.Depuis Python 3.11, asyncio.TaskGroup est la forme recommandée :async with asyncio.TaskGroup() as tg:
tg.create_task(telecharger("A"))
tg.create_task(telecharger("B"))
- Entrées-sorties (réseau, API, base de données) : asyncio brille, car le programme passe son temps à attendre.
- Calcul intensif (CPU) : asyncio n'aide pas, tout reste sur un seul thread. Utilisez le multiprocessing.
- Script simple et séquentiel : le code synchrone suffit, inutile de compliquer.
asyncio.TaskGroup (Python 3.11) est un gestionnaire de contexte qui lance plusieurs tâches et attend qu'elles se terminent toutes :async with asyncio.TaskGroup() as tg:
tg.create_task(telecharger("A"))
tg.create_task(telecharger("B"))
# ici, les deux tâches sont finies
Son avantage sur gather : si une tâche échoue, les autres sont annulées automatiquement et l'erreur est propagée proprement. C'est le principe de la concurrence structurée, plus sûr pour la gestion des erreurs.Prochaines étapes
Section intitulée « Prochaines étapes »L'asynchrone prend tout son sens sur les appels réseau et se retrouve dans de nombreux outils modernes.