async/await transfers almost 1:1 from JS, but Python's event loop is not ambient. Two gotchas from wiring up token-by-token streaming.
You start it
async/await, but the event loop is not ambient anymore
This is post 4 of my TypeScript-to-Python journey. The event loop I have trusted for ten years, except this time Python handed me the keys and said "you start it."
Here's the mistake that taught me the lesson:
ask_model is an async def, so calling it like a normal function doesn't run it. answer isn't a string, it's a coroutine object: <coroutine object ask_model at 0x1046...>. No request fired, no error either, just that printed instead of the response I expected, and a RuntimeWarning: coroutine 'ask_model' was never awaited a few lines later in the output.
In JS the equivalent slip is harmless: forget an await and you still get a Promise you can catch later. In Python the function body never even started executing. The fix is one keyword, answer = await ask_model(...), but the failure mode is the interesting part: it doesn't crash, it just quietly does nothing.
That mismatch is the whole lesson of this week: in Node the event loop is ambient. It is always running, top-level await just works and most libraries are async by default. In Python nothing is running until you write asyncio.run(main()). The language keeps a sync world and an async world side by side, and you decide when to enter.
Once that clicked, the syntax itself was the easy part. async def instead of async function, await is still await, Promise.all becomes asyncio.gather. Ten years of JS intuition mostly still applies, it just needed an explicit door to walk through.
I was trying to call an LLM and stream LLM's answer to the terminal token by token, the thing behind every chat UI you've used. Under the hood it's server-sent events, one held-open connection delivering small deltas and consumed with async for, which is just a for-loop that awaits between iterations.
async with opens the connection and guarantees it closes even if something errors mid-stream, same idea as a try/finally around a socket.
First run, the words still arrived in bursts instead of flowing. The problem was not the API, not async, just Python buffering stdout by default.
Add flush=True and it types like a real chat UI, first words on screen in under a second, even when the full answer takes twenty.
I also learn one important lesson that streaming does not make the model generate faster. Total time is roughly the same. It just moves the first token from second twenty to second one, and that gap is the entire difference between an app feeling alive and feeling broken.
The syntax was free. The runtime model, ambient versus explicit, was the actual lesson, and explicit is turning out to be easier to reason about than I expected.
Stop writing JavaScript in Python
Comprehensions, a reversed join(), and KeyError
List comprehensions, a reversed join(), and KeyError instead of undefined. Small syntax, but the idioms took real unlearning.
Zod taught me Pydantic
Define the schema, parse at the boundary. Same mental model, new language.
If you have used Zod, you already understand 70 to 80% of Pydantic. The mental model transfers almost one-to-one.
Python has an npm now
uv, from a Node dev's point of view
Coming from npm and node_modules, Python tooling felt alien until I found uv. Here is the 1:1 mapping that made it click.