Switching from JavaScript to Python feels like moving to a city with the same street signs but different traffic laws. The syntax looks friendly and familiar. You see async and await sitting right there in the grammar, so you assume the mental model ports over cleanly. It does not. One habitual pattern from JavaScript silently tanks your Python performance without crashing, without logging an error, and without showing up on a quick code review.
How JavaScript Teaches You to Start and Forget
In JavaScript, an async function call is eager. The moment you invoke it, the engine creates a Promise and the work begins immediately. The event loop is already off to the races. That is why JavaScript developers naturally write code like this:
const userPromise = fetchUser(id);
const ordersPromise = fetchOrders(id);
const user = await userPromise;
const orders = await ordersPromise;
Both network requests are in flight before either await is reached. The first await suspends the current function until fetchUser resolves, but fetchOrders has been humming along in the background since the previous line. By the time you need the orders variable, the second request might already be done. This pattern feels so natural in JavaScript that many developers do not even think of it as a concurrency trick. It is just how async works.
The Python Surprise: A Cold Coroutine
Python uses a different contract. When you call an async def function in Python, you do not start any work. You receive a coroutine object. Think of it as a recipe written on paper. The ingredients are listed, the steps are clear, but nothing is in the oven. Until something explicitly drives that coroutine through the event loop, it remains inert.
Here is the trap. A JavaScript engineer who needs a user and their orders might write this in Python:
user_coro = fetch_user(id)
orders_coro = fetch_orders(id)
user = await user_coro
orders = await orders_coro
It looks concurrent. It smells concurrent. It is entirely sequential.
The first line assigns a dormant coroutine to user_coro. The second line assigns another dormant coroutine to orders_coro. When execution hits await user_coro, Python finally starts the first task and runs it to completion. Only after fetch_user finishes does the interpreter reach await orders_coro and start the second task. Your total execution time is the sum of both I/O operations, not the longest one. You did not run them in parallel. You ran them one after another with extra steps.
Why This Bug Is Invisible
This is the kind of performance regression that survives for months. The code is valid Python. It passes type checkers. It returns the correct results. It just runs at half speed, or worse. Because there is no stack trace and no warning, engineering teams often look everywhere else first. They add Redis caches, upgrade database tiers, or switch hosting regions. The real culprit is a subtle mismatch in expectations about what await actually does.
Three Ways to Make Python Actually Run Things Concurrently
To fix this, you must tell Python’s event loop to schedule the work immediately. You need something more active than a raw coroutine. You need a Task.
1. asyncio.create_task
The most direct translation of the JavaScript pattern is to wrap your coroutine in a Task. A Task is scheduled on the event loop as soon as you create it. It is the closest Python equivalent to a JavaScript Promise in motion.
user_task = asyncio.create_task(fetch_user(id))
orders_task = asyncio.create_task(fetch_orders(id))
user = await user_task
orders = await_orders_task
Now both fetch_user and fetch_orders are in flight before the first await. When you reach await user_task, you pause only until that specific Task completes, but the other Task keeps running. If fetch_orders finishes first, its result simply waits inside orders_task until you ask for it.
Be careful, though. If you create a Task and never await it, Python will emit an error about a destroyed pending task. You must still collect your results.
2. asyncio.gather
If you have several coroutines that all need to finish before you move on, asyncio.gather handles the boilerplate for you. It schedules each coroutine as a Task internally and awaits them together.
user, orders = await asyncio.gather(fetch_user(id), fetch_orders(id))
This is concise and readable. It shines when the operations are independent and you want a single line that expresses "run all of these, then give me every result." It also preserves the order of arguments in the returned list or tuple, even if the underlying tasks complete in a different order.
3. asyncio.TaskGroup
Python 3.11 मध्ये TaskGroup सादर करण्यात आले आहे, जे स्टँडर्ड लायब्ररीमध्ये स्ट्रक्चर्ड कॉनकरन्सी (structured concurrency) आणते. टास्क मॅन्युअली तयार करण्याऐवजी, तुम्ही एक कॉन्टेक्स्ट मॅनेजर (context manager) वापरता जो प्रत्येक तयार केलेला टास्क व्यवस्थित पूर्ण होईल याची खात्री देतो. जर एखाद्या टास्कमध्ये एक्सेप्शन (exception) आले, तर इतर टास्क आपोआप रद्द केले जातात.
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()
हा पॅटर्न क्लिष्ट वर्कफ्लोसाठी उत्कृष्ट आहे. यामुळे टास्क ऑर्फन (orphan) होण्याचा धोका टळतो आणि संबंधित ऑपरेशन्सचे लाइफसायकल एकाच लॉजिकल छत्राखाली येते. जर तुमचा कोडबेस Python 3.11 किंवा त्यापेक्षा नवीन व्हर्जनवर चालत असेल, तर फॅन-आउट कॉनकरन्सी (fan-out concurrency) साठी ही अनेकदा सर्वात स्वच्छ आर्किटेक्चर असते.
मेंटल मॉडेल: await म्हणजे "हे आता चालवा"
मुख्य धडा भाषिक आहे. JavaScript मध्ये, तुम्ही await चा अर्थ "दरम्यान" (meanwhile) असा घेऊ शकता. तुम्ही काम सुरू करता, इतर गोष्टी करता आणि जेव्हा तुम्हाला व्हॅल्यूची गरज असते तेव्हाच थांबता. Python मध्ये, await म्हणजे "या कोरुटीनला (coroutine) त्याच्या पुढच्या सस्पेंशन पॉईंटपर्यंत किंवा पूर्णत्वापर्यंत नेणे." जर कोरुटीन अजून शेड्यूल केले नसेल, तर await ते शेड्यूल करण्याचे काम करते. म्हणूनच तुम्ही दोन रॉ कोरुटीन्स सुरू करून नंतर त्यांना await करू शकत नाही. तुम्ही दरम्यानच्या काळात इव्हेंट लूपला (event loop) काहीही करण्यासाठी दिलेले नसते.
Python कोरुटीन्सना जनरेटर फंक्शन्स (generator functions) प्रमाणे समजा. जनरेटर कॉल केल्याने त्याचे इटरेशन होत नाही. तुम्हाला त्यावर लूप फिरवावा लागेल, next() कॉल करावा लागेल किंवा ते एखाद्या कंज्युमरकडे (consumer) पास करावे लागेल. Async देखील याच पद्धतीने काम करते. asyncio.create_task हा असा कंज्युमर आहे जो म्हणतो "हे आत्ताच इव्हेंट लूपवर टाका." त्यानंतरचा await फक्त काम पूर्ण झाल्याच्या सिग्नलची वाट पाहतो.
एक उपयुक्त सवय: जेव्हा तुम्ही await शिवाय एखाद्या async फंक्शन कॉलला व्हेरिएबलमध्ये असाइन करता, तेव्हा स्वतःला विचारा की तुम्ही ते शेड्यूल केले आहे का. जर उजव्या बाजूचा भाग create_task, gather, किंवा TaskGroup मध्ये गुंडाळलेला नसेल, तर ते रन होत नाहीये. ते फक्त काउंटरवर ठेवलेल्या रेसिपीसारखे आहे.
निष्कर्ष
Python चे async रनटाइम शक्तिशाली आहे, परंतु त्यासाठी स्पष्ट हेतूची (explicit intent) आवश्यकता असते. तुम्ही केवळ एखादे फंक्शन कॉल केले म्हणून भाषा बॅकग्राउंड वर्क सुरू करत नाही. जर तुम्ही JavaScript मधून येत असाल, तर जिथे तुम्ही कोरुटीन व्हेरिएबलमध्ये स्टोअर करता आणि नंतर त्याला await करता, अशा प्रत्येक ठिकाणाची तपासणी (audit) करा. जोपर्यंत तुम्ही त्याला आधी टास्कमध्ये रूपांतरित (promote) करत नाही, तोपर्यंत तुम्ही 'async' च्या वेषात सिक्वेन्शिअल कोड लिहिला आहे. कामाची सुरुवात टास्कने करा, आणि नंतर निकालाची वाट पहा. अशा प्रकारे तुम्ही Python async ला एका शांत अडथळ्यापासून (silent bottleneck) खऱ्या कॉनकरन्सी टूलमध्ये बदलू शकता.
