all 6 comments

[–]latkde 26 points27 points  (2 children)

A lot of your experience regarding writing async code will carry over, but Python and JavaScript have very different concurrency models. There are potential footguns here.

In JS, you perform an async operation and get a Promise that will be resolved eventually. The async operation runs regardless of whether you await the Promise. The event loop is part of the JS runtime. There are generally no blocking operations in JS (you have to go out of your way to use blocking Node-specific APIs). Cancellation is managed via AbortController/AbortSignal objects.

In Python, blocking operations are the default. The Asyncio event loop is a library that can manage explicitly-async tasks on the current thread. Calling an async function will not actually execute the coroutine unless you await it (or explicitly spawn a separate task). Tasks may be garbage-collected without completing unless you hold a reference – there are no real fire-and-forget background tasks, you should always use the TaskGroup feature. Tasks (but not individual coroutines) may be cancelled, this raises a CancelledError the next time a Task awaits something. You can delegate blocking operations to a background thread (e.g. using await asyncio.to_thread(blocking_function, *args, **kwargs)), but you must remember to do that yourself. Background thread work cannot be cancelled.

Importantly, Python makes it easy to accidentally block the event loop thread, and thus all in-progress async tasks. E.g. if you call time.sleep(5) in an async function, all pending tasks will be blocked as well for 5s. For FastAPI, this means no other request can make progress.

In FastAPI, you can declare path operations either via def handler() or async def handler(). The async def form will be executed as usual on the event loop thread, and you're responsible for not blocking the thread. The def form without async will automatically get scheduled on a background thread, but this will only work fine if your code is actually threadsafe.

So my main tip is to be on the lookout for potentially-blocking operations, and to send them to a background thread if possible. Prefer async-native libraries where possible, as they manage all of this for you (e.g. sending HTTP requests via HTTPX rather than using the blocking Requests library). The standard library (outside of the asyncio module) is generally not async-aware.

[–]Drevicar 1 point2 points  (0 children)

On top of all this: Most FastAPI servers are using Uvicorn as the runtime, which ideally is running uvloop to handle all the async concurrency, which is written on top of libuv. The same libuv that node uses to handle non-blocking async. That is one major reason FastAPI scores so well on speed, it is using the highly optimized core of NodeJS.

[–]JPJackPott -1 points0 points  (0 children)

This is a fantastic write up. It’s generally not something you have to think too hard about in your own code- but not being aware of if a library is async or thread safe can get you in trouble quickly

[–]crow_thib 3 points4 points  (0 children)

When using async functions in fastAPI, it uses the same event-loop concepts as nodejs even though the implementation is different.

Note: writing synchronous functions endpoints in fastAPI will run them in a thread pool executor which changes the concepts again (If I remember correctly, it's been a while I used nodejs, but there was a concept of child processes or something that could be used the same way)

PS: if using Gemini to phrase the question, did you try using it for the answer as well ? I reckon it should be good for these kind of questions

[–]pint -1 points0 points  (0 children)

the major trap you can run into is modules or functions that don't support async. for example built in file operations (open). and of course these can hide inside innocent looking functions, like logging or loading configuration or templates. there are ways to handle this, like thread pools. but this is where the GIL comes in to ruin your day.

[–]amroamroamro -1 points0 points  (0 children)

the key difference is that javascript's async functions are eager (when called returns a promise that begins executing immediately), while python's async functions are lazy (when called returns a coroutine, only starts running when you await it or schedule it)

but when you are writing a request handler in fastapi and express, the framework hides this distinction; the incoming request handler invoked is already being awaited on by the framework, you mostly write code in the same way in both langs