I mean this earnestly: if you want to pay little performance cost on the sync flow to support your async API, what approaches have libraries taken that you have seen?
What I have seen done (purely annecdotal):
- new libraries that just make “everything” async def, and don’t have sync APIs. At least then people subclassing your library class can use async libs!
- libraries that do codegen (psycopg3being my canonical example, where the sync variant is generated from the async variant)
- hard splits of I/O from state machine logic (see wsproto) where you really embrace “split logic from I/O”, with all the API consequences that entails (and we don’t have monads to make command pattern code look imperative! We do have generators of course…)
- The “async version” of a previously sync library, which is a just a different lib (often written by different people!), where someone took the original lib, and replaced defwithasync def.
- People reaching for geventand friends (though it’s not clear to me if that actually resolves the problem)
ranty non-facts now:
When I’ve discussed this stuff with people, nobody seems to have a satisfactory answer. On one discussion here codegen + “functional core imperative shell” was mentioned. Rust, when faced with a similar problem, started an initiative to have “maybe_async” support (which has made some progress not not much and their problems are slightly different).
Javascript has “solved” the problem by only doing async APIs for things (on the “nothing should ever block” principle).
Like if someone has a good inventory of clean ways to support an imperative API that doesn’t boil down to “spin up an event loop and use that to call into the async version” then they should give a talk on it, because I think a lot of lib developers would be interested, not just Python people!
Or maybe that’s just the answer and it’s actually performant enough to not matter. But even then, asyncio’s lack of re-entrancy means that you run into the color problem if within an async call you go back to sync world and then try to go back into async world. This is a problem for the frameworks because frameworks get called, but then call back into user code.
Usual disclaimer with my rants: I might be missing something extremely obvious. I am definitely a bit in the weeds after having looked at Django’s issues in particular on this topic for a while.