Async in Python vs JavaScript: What Changes When You Switch
Switching from JavaScript to Python, it’s easy to assume the two languages are basically the same with different syntax. Both are dynamically typed, both treat functions as first-class objects, both support OOP. The syntax differs but feels familiar. Lately I’ve been doing more Python, so I started paying attention to the small differences. Today I want to look at how async handling actually differs.
Defining an async function
In Python you define an async function much like in JavaScript:
async def foo():
...
The difference is that the async code inside won’t actually run until you await it. Unlike Node.js, async doesn’t wrap the body in a Promise that starts executing immediately. Instead, it creates a coroutine that runs only once you await it.
So this won’t create a user:
async def foo():
create_user()
But this will:
async def foo():
await create_user()
You also need to kick everything off with asyncio.run:
asyncio.run(foo())
Does calling asyncio.run create a new event loop?
Yes. Call it twice and you get two event loops. In an application you want a single main asyncio.run call and you use async/await inside it. Don’t reach for asyncio.run to fire off many small async functions one by one.
In older Node, before top-level await, we used an IIFE to do the same thing. asyncio.run plays a similar role:
(async () => {
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts(),
]);
console.log(user, posts);
})();
Fire and forget
In JavaScript you can fire an async function and forget about it, or do other work and await it later:
async function foo() {
const promise = createUser();
// other code
const result = await promise;
}
Because of how Python handles coroutines, you have to convert the coroutine into a Task to get the same effect. You do that with asyncio.create_task:
async def foo():
task = asyncio.create_task(create_user())
# other code
result = await task
Here’s how that plays out on the event loop. foo schedules the task, keeps doing its own work, then parks on await task until the background work returns.
async def foo():task = asyncio.create_task(create_user())do_other_work()result = await task
Running functions concurrently
In JavaScript, when you want to make multiple I/O calls in parallel, you reach for Promise.all. You pass it an array of Promises and it returns one Promise that resolves when all of them succeed:
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts(),
]);
In Python the built-in equivalent is asyncio.gather:
import asyncio
user, posts = await asyncio.gather(
fetch_user(),
fetch_posts(),
)
Watching it run, both fetches go from ready to waiting almost immediately and resolve independently. Total time is roughly the slower of the two, not their sum.
user, posts = await asyncio.gather(fetch_user(),fetch_posts(),)
Delaying without blocking the event loop
In JavaScript, when you want to wait some time, you reach for setTimeout(fn, ms), which runs fn after ms milliseconds. setTimeout registers a callback that the event loop will invoke later.
In Python you use await asyncio.sleep(seconds). It pauses the current coroutine for the given number of seconds without blocking the whole event loop. After await, the coroutine yields control back to the loop, which can resume it later.
The practical effect:
- In JS, code after
setTimeoutkeeps running immediately. - In Python, code after
await asyncio.sleep(...)in the same coroutine waits, but other tasks and coroutines can keep going.
Example:
console.log("A");
setTimeout(() => console.log("B"), 1000);
console.log("C");
Output: A, C, then B.
import asyncio
async def main():
print("A")
await asyncio.sleep(1)
print("B")
print("C")
asyncio.run(main())
Output: A, then B after a second, then C.
Press play and watch fast finish while slow is still parked on sleep(2). That’s the event loop picking up whichever task is ready, instead of blocking on the sleeping one.
async def slow():await asyncio.sleep(2)print('slow done')async def fast():print('fast done')
Wrapping up
Python feels very similar to JavaScript when it comes to async functions, but there are small differences that meaningfully change how those functions behave and how you have to reason about them. It’s worth being aware of them so you’re not caught off guard when something that worked out of the box in JavaScript needs a slightly different approach in Python.