Python async / wait expliqué : le didacticiel 2026
Async en Python a la réputation d'être déroutant. Ce n’est pas obligatoire. Une fois que vous avez internalisé une phrase — async permet à UN thread d'exécuter plusieurs tâches liées aux E/S entrelacées — tout le reste suit.
Ce didacticiel couvre : ce que font réellement async def et await, quand async gagne ou quand ce n'est pas le bon outil, les quatre modèles utilisés par chaque base de code async (séquentiel, rassemblement, sémaphore, délai d'attente) et les trois façons dont les débutants avalent silencieusement les erreurs.
Le modèle mental en une phrase
Async en Python est une concurrence coopérative dans un seul thread. Lorsque votre code atteint « attendre », le contrôle revient à la boucle d'événements, qui peut exécuter une autre tâche pendant que la première attend les E/S. Une fois l'E/S terminée, la boucle reprend la première tâche là où elle s'était arrêtée.
C'est ça. Pas de threads, pas de processus, pas de parallélisme (au sens CPython-with-GIL). Un seul thread jonglant avec de nombreuses tâches qui passent la plupart de leur temps en attente sur le réseau/disque/base de données.
Async est plus rapide lorsque votre goulot d'étranglement attend, pas lorsque vous calculez.
Le programme asynchrone minimum viable à trois lignes
async def hello()— déclare une coroutine. Appelerhello()n'exécute pas le code ; il crée un objet coroutine.await asyncio.sleep(1)— redonne le contrôle à la boucle d'événements pendant 1 seconde.asyncio.run(hello())— crée une boucle d'événements et exécute la coroutine jusqu'à son terme.
Règle : `await` uniquement à l'intérieur de `async def`. Appeler « wait » dans une fonction régulière est une « SyntaxError ».
Quand l'asynchrone gagne (et quand ce n'est pas le cas)
Async gagne : travail lié aux E/S
Toute tâche qui passe la majeure partie de son temps d'horloge murale à ATTENDRE est candidate à l'async :
- Requêtes HTTP (APIs externes, scraping, webhooks)
- Requêtes de base de données
- Lectures/écritures de fichiers
- Flux WebSocket/gRPC
- Consommateurs de file d'attente (Redis, RabbitMQ, Kafka)
Exemple : récupérer 100 URL.
Accélération de l'horloge murale 50 à 100x pour les travaux gourmands en E/S. C'est là que l'async gagne son budget de complexité.
Async PERD : travail lié au processeur
Async ne vous offre aucun parallélisme pour le calcul. Puisqu'il s'agit d'un système coopératif monothread, une boucle chauffée par le processeur bloque toute la boucle d'événements.
Toutes les autres tâches meurent de faim pendant l'exécution de « pbkdf2 ». Pour le travail lié au processeur, optez pour « multitraitement » ou « concurrent.futures.ProcessPoolExecutor », et non asynchrone.
Charge de travail mixte : run_in_executor
Si UNE étape est gourmande en ressources CPU et que le reste est constitué d'E/S, enveloppez l'étape CPU afin qu'elle s'exécute dans un pool de threads sans bloquer la boucle :
Le Aucun = pool de threads par défaut. Idéal pour appeler des bibliothèques de synchronisation tierces qui sont lentes mais que vous ne pouvez pas réécrire.
Les quatre modèles
1. Séquentiel : une tâche à la fois
À utiliser lorsque chaque requête dépend du résultat de la précédente.
2. asyncio.gather — déclenche tout d'un coup
Choix par défaut pour les tâches d'E/S indépendantes. Mais : si vous rassemblez 10 000 tâches sur une seule API, vous obtiendrez des descripteurs de fichiers limités ou épuisés. C'est là qu'intervient le modèle de sémaphore.
3. Sémaphore — limitation de la concurrence
asyncio.Semaphore(N) permet à au plus N tâches de dépasser async with sem: à tout moment. Les autres tâches sont en file d'attente. À utiliser pour toute API externe avec une limite de débit ou une base de données avec un pool de connexions.
4. Timeout : abandonnez le travail lent
asyncio.wait_for annule la tâche sous-jacente après N secondes. Essentiel pour tout code asynchrone qui communique avec un réseau : ne faites jamais d'attente illimitée.
Les trois pièges avaleurs d'erreurs
Piège 1 : asyncio.gather avec return_exceptions=True
Utile lorsque vous souhaitez conserver des résultats partiels mais MORTAL si vous ne vérifiez pas tous les résultats :
Sans vérification, les erreurs disparaissent silencieusement.
Piège 2 : tâches de lancement et d'oubli
Si background_work() se déclenche, Python émet un avertissement à l'arrêt et continue. Pour tout ce qui vous intéresse, stockez la tâche et « attendez-la » ou utilisez « asyncio.TaskGroup » (3.11+) qui propage toutes les erreurs.
Piège 3 : Synchroniser le code dans l'asynchrone
requests est synchrone. Même à l'intérieur d'un async def, requests.get bloque toute la boucle jusqu'à ce que la réponse arrive. Utilisez httpx (ou aiohttp) pour le HTTP asynchrone.
Règle : n'utilisez jamais de bibliothèque d'E/S synchrone dans du code asynchrone. Si la bibliothèque n'a pas de variante asynchrone, enveloppez-la dans run_in_executor.
asyncio.TaskGroup — la façon 2026 de se déployer
Python 3.11 a ajouté « TaskGroup », qui résout proprement le problème du feu et de l'oubli :
Concurrence structurée : pas d'erreurs perdues, pas de tâches orphelines. Préférez cela au simple create_task dans le code moderne.
Exemple concret : scraper à débit limité
Les quatre modèles sur 15 lignes : concurrence limitée (sémaphore), délais d'attente (wait_for), branches d'erreur explicites (pas d'avalement) et collecte pour la distribution. Copiez-collez cette forme dans n'importe quelle tâche d'E/S asynchrone.
Async en Python a une véritable courbe d'apprentissage, mais le modèle mental est simple : un thread, des pauses coopératives à « attendre », des chevauchements d'E/S. Maîtrisez gather + Semaphore + wait_for + TaskGroup et vous aurez couvert 95 % du travail asynchrone que vous écrirez.
Prochaine étape : le Senior Python track propose 20 leçons pratiques sur asyncio, TaskGroup, aiohttp/httpx et les modèles de concurrence ci-dessus, avec du vrai Python exécuté dans votre navigateur.