Today I took a closer look at Python's asyncio from my JavaScript perspective. Overall, it's feels weirdly familiar given Node's async / await, but also has some quirky differences.

Event Loop

Both Python and Node manage concurrency with an event loop. Here's a quick mapping:

Concept Node.js Python
Event loop Built-in (node script.js) asyncio.run(main()) (Python +3.7)
Access event loop Rarely needed asyncio.get_running_loop()
Nested event loops Not a concern Can't nest calls to asyncio.run()

Typically, Python 3.7+ encourages using asyncio.run() to manage everything neatly.

Coroutines ≈ JS Async Functions

Coroutines in Python are essentially like JavaScript's async functions:

import asyncio

async def fetch_url(url):
    print(f'Starting to fetch {url}')
    await asyncio.sleep(1)
    print(f'Finished fetching {url}')
    return f'{url} - {len(url)} bytes'

Just like JS, using await gives control back to the event loop until the coroutine finishes.

Running Coroutines

Single coroutine:

asyncio.run(fetch_url('https://andypai.me'))

Multiple concurrent coroutines (like Promise.all):

async def main():
    await asyncio.gather(
        fetch_url('https://til.andypai.me'),
        fetch_url('https://neuroflow.andypai.me'),
        fetch_url('https://tensorflowjs.andypai.me')
    )

asyncio.run(main())

Background Tasks (basically setInterval)

To run something repeatedly in the background:

async def do_ticks():
    while True:
        print('Tick')
        await asyncio.sleep(5)

async def main():
    asyncio.create_task(do_ticks())
    await asyncio.sleep(12)

asyncio.run(main())

Structured Concurrency (TaskGroup in Python 3.11+)

Python 3.11 introduced structured concurrency, similar to JS's Promise.allSettled:

async def main():
    async with asyncio.TaskGroup() as tg:
        for url in urls:
            tg.create_task(fetch_url(url))

If one task errors, it neatly cancels the others and raises an ExceptionGroup.

Python/JS Async Equivalents Cheat Sheet

JavaScript Python asyncio
Promise.all asyncio.gather()
Promise.race asyncio.wait(return_when=FIRST_COMPLETED)
setTimeout(fn, 0) loop.call_soon(fn)
Worker threads (CPU tasks) loop.run_in_executor() or multiprocessing
Canceling tasks task.cancel() or asyncio.TimeoutError

Quick Example: Async HTTP Requests

Python's async HTTP client (aiohttp) feels similar to Node's fetch API:

import asyncio, aiohttp

URLS = [
    'https://finbox.io/1',
    'https://finbox.io/2',
    'https://finbox.io/3'
]

async def fetch(session, url):
    async with session.get(url) as resp:
        print(url, resp.status)
        return await resp.text()

async def main():
    async with aiohttp.ClientSession() as session:
        results = await asyncio.gather(*(fetch(session, url) for url in URLS))
        print('Total bytes:', sum(len(r) for r in results))

asyncio.run(main())

JavaScript equivalent (roughly):

const fetch = require('node-fetch')
await Promise.all(urls.map(u => fetch(u)))

So yeah, Javascript is way cleaner for async and io imho.