Passer de JavaScript à Python, c'est comme déménager dans une ville qui possède les mêmes panneaux de signalisation, mais des règles de circulation différentes. La syntaxe semble conviviale et familière. On voit async et await directement dans la grammaire, alors on suppose que le modèle mental se transpose sans problème. Ce n'est pas le cas. Un schéma habituel de JavaScript peut saboter silencieusement vos performances en Python, sans faire planter le programme, sans générer d'erreur dans les logs et sans apparaître lors d'une revue de code rapide.

Comment JavaScript vous apprend à démarrer et à oublier

En JavaScript, l'appel d'une fonction async est immédiat (eager). Dès que vous l'invoquez, le moteur crée une Promise et le travail commence immédiatement. La boucle d'événements (event loop) est déjà lancée. C'est pourquoi les développeurs JavaScript écrivent naturellement du code comme ceci :

const userPromise = fetchUser(id);
const ordersPromise = fetchOrders(id);
const user = await userPromise;
const orders = await ordersPromise;

Les deux requêtes réseau sont lancées avant même d'atteindre le premier await. Le premier await suspend la fonction actuelle jusqu'à ce que fetchUser soit résolu, mais fetchOrders tourne déjà en arrière-plan depuis la ligne précédente. Au moment où vous avez besoin de la variable orders, la seconde requête est peut-être déjà terminée. Ce schéma semble si naturel en JavaScript que de nombreux développeurs ne le considèrent même pas comme une astuce de concurrence. C'est simplement ainsi que fonctionne async.

La surprise Python : une coroutine froide

Python utilise un contrat différent. Lorsque vous appelez une fonction async def en Python, vous ne lancez aucun travail. Vous recevez un objet coroutine. Voyez cela comme une recette écrite sur papier. Les ingrédients sont listés, les étapes sont claires, mais rien n'est dans le four. Tant que rien ne fait passer explicitement cette coroutine par la boucle d'événements, elle reste inerte.

Voici le piège. Un ingénieur JavaScript qui a besoin d'un utilisateur et de ses commandes pourrait écrire ceci en Python :

user_coro = fetch_user(id)
orders_coro = fetch_orders(id)
user = await user_coro
orders = await orders_coro

Cela ressemble à de la concurrence. Cela a l'air d'être de la concurrence. C'est pourtant entièrement séquentiel.

La première ligne assigne une coroutine dormante à user_coro. La deuxième ligne assigne une autre coroutine dormante à orders_coro. Lorsque l'exécution atteint await user_coro, Python commence enfin la première tâche et l'exécute jusqu'au bout. Ce n'est qu'une fois fetch_user terminé que l'interpréteur atteint await orders_coro et lance la seconde tâche. Votre temps d'exécution total est la somme des deux opérations d'E/S, et non la durée de la plus longue. Vous ne les avez pas exécutées en parallèle. Vous les avez exécutées l'une après l'autre, avec des étapes superflues.

Pourquoi ce bug est invisible

C'est le genre de régression de performance qui survit pendant des mois. Le code est du Python valide. Il passe les vérificateurs de type. Il renvoie les bons résultats. Il tourne simplement à mi-vitesse, voire moins. Comme il n'y a ni trace de pile (stack trace) ni avertissement, les équipes d'ingénierie cherchent souvent ailleurs en premier. Elles ajoutent des caches Redis, montent en gamme de base de données ou changent de région d'hébergement. Le véritable coupable est un décalage subtil dans les attentes concernant ce que await fait réellement.

Trois façons de rendre Python réellement concurrent

Pour corriger cela, vous devez dire à la boucle d'événements de Python de planifier le travail immédiatement. Vous avez besoin de quelque chose de plus actif qu'une simple coroutine. Vous avez besoin d'une Task.

1. asyncio.create_task

La traduction la plus directe du modèle JavaScript consiste à encapsuler votre coroutine dans une Task. Une Task est planifiée sur la boucle d'événements dès sa création. C'est l'équivalent Python le plus proche d'une Promise JavaScript en mouvement.

user_task = asyncio.create_task(fetch_user(id))
orders_task = asyncio.create_task(fetch_orders(id))

user = await user_task
orders = await_orders_task

Désormais, fetch_user et fetch_orders sont tous deux lancés avant le premier await. Lorsque vous atteignez await user_task, vous ne faites une pause que jusqu'à ce que cette Task spécifique soit terminée, mais l'autre Task continue de s'exécuter. Si fetch_orders se termine en premier, son résultat attend simplement à l'intérieur de orders_task jusqu'à ce que vous le demandiez.

Attention toutefois. Si vous créez une Task sans jamais faire de await dessus, Python émettra une erreur concernant une tâche en attente détruite. Vous devez tout de même récupérer vos résultats.

2. asyncio.gather

Si vous avez plusieurs coroutines qui doivent toutes se terminer avant de continuer, asyncio.gather gère le code répétitif pour vous. Il planifie chaque coroutine en tant que Task en interne et les attend toutes ensemble.

user, orders = await asyncio.gather(fetch_user(id), fetch_orders(id))

C'est concis et lisible. Cela brille lorsque les opérations sont indépendantes et que vous voulez une seule ligne exprimant "exécutez tout ceci, puis donnez-moi chaque résultat". Cela préserve également l'ordre des arguments dans la liste ou le tuple renvoyé, même si les tâches sous-jacentes se terminent dans un ordre différent.

3. asyncio.TaskGroup

Python 3.11 a introduit TaskGroup, qui apporte la concurrence structurée à la bibliothèque standard. Au lieu de créer des tâches manuellement, vous utilisez un gestionnaire de contexte qui garantit que chaque tâche lancée se termine correctement. Si une tâche lève une exception, les autres sont automatiquement annulées.

async with asyncio.TaskGroup() as tg:
    user_task = tg.create_task(fetch_user(id))
    orders_task = tg.create_task(fetch_orders(id))

user = user_task.result()
orders = orders_task.result()

Ce modèle est excellent pour les flux de travail complexes. Il élimine le risque de laisser une Task orpheline et regroupe le cycle de vie d'opérations liées sous un même parapluie logique. Si votre base de code fonctionne sous Python 3.11 ou une version ultérieure, il s'agit souvent de l'architecture la plus propre pour la concurrence de type fan-out.

Le modèle mental : await signifie « exécutez ceci maintenant »

La leçon fondamentale est linguistique. En JavaScript, on peut lire await comme « en attendant ». Vous lancez un travail, faites d'autres choses, et ne faites une pause que lorsque vous avez besoin de la valeur. En Python, await signifie « menez cette coroutine jusqu'à son prochain point de suspension ou jusqu'à son achèvement ». Si la coroutine n'a pas encore été planifiée, c'est await qui s'en charge. C'est pourquoi vous ne pouvez pas démarrer deux coroutines brutes pour ensuite les attendre (await) plus tard. Vous n'avez rien donné à faire à la boucle d'événements entre-temps.

Considérez les coroutines Python comme des fonctions génératrices. Appeler le générateur ne l'itère pas. Vous devez boucler dessus, appeler next() ou le passer à un consommateur. L'asynchrone fonctionne de la même manière. asyncio.create_task est le consommateur qui dit : « placez ceci sur la boucle d'événements dès maintenant ». Le await suivant ne fait qu'attendre le signal de fin.

Une habitude concrète qui aide : chaque fois que vous assignez l'appel d'une fonction asynchrone à une variable sans await, demandez-vous si vous l'avez planifiée. Si le côté droit n'est pas enveloppé dans create_task, gather ou TaskGroup, il n'est pas en cours d'exécution. C'est juste une recette posée sur le comptoir.

À retenir

L'environnement d'exécution asynchrone de Python est puissant, mais il exige une intention explicite. Le langage ne lance pas de travail en arrière-plan simplement parce que vous avez appelé une fonction. Si vous venez de JavaScript, auditez chaque endroit où vous stockez une coroutine dans une variable pour l'attendre (await) plus tard. À moins de l'avoir d'abord promue en Task, vous avez écrit du code séquentiel habillé en asynchrone. Lancez le travail avec une Task, puis attendez les résultats. C'est ainsi que vous transformez l'asynchrone Python d'un goulot d'étranglement silencieux en un véritable outil de concurrence.