A split bigger than the decade long transition from 2 to 3 is well underway in the Python community. Coroutines and async/await syntax have existed in Python since version 3.5 released in 2015, but tools and projects utilizing the feature were slow to be released. Mature libraries to build async applications are starting to become available and the libraries we love are moving in the async direction. Async is the future of Python.
Why it matters
Asyncio is the biggest change since the language’s introduction. It is a library and additional syntax that programmers use to mark their python code. The syntax tells what functions are making IO calls allowing the interpreter to schedule concurrent tasks on a single thread. Without asyncio, when python code waits for network responses, like from a database, it blocks the whole thread.
Anything that introduces latency and blocks the thread needs to be labeled as a coroutine. Database connectors, caches, ORMs, web frameworks and a lot more all need to be rewritten. The fastest web frameworks in Python use asyncio. However, their adoption has been slowed because the lack of companion libraries.
What it is replacing
Concurrency in Python used to be invisible. It is primarily needed for web frameworks, and we have a great generalized tool to run those: gunicorn. Gunicorn is an HTTP server which runs an WSGI application, a specification which Django and Flask use. It could achieve concurrency by utilizing gevent. Gevent patches all network calls to avoid blocking the thread in much of the same way asyncio does. Gevent patches the calls automatically, making ORMs, caches and everything else work with zero work required. It is monkey-patching the language instead of letting the interpreter handle concurrency natively. This was said in a Django developer document about gevent:
While the idea of having methods and functions seems attractive at first, there are many subtle problems with the greenlet-based approach. The lack of an explicit
awaitmeans that a complex API like Django's basically becomes unpredictable as to knowing if it will block the current execution context or not. This then leads to a much higher risk of race conditions and deadlocks without careful programming, something I have experienced first-hand.
The State of Asyncio
Asyncio is most useful in web frameworks and we are starting to see many mature ones arrive. Databases and ORMs are more complicated and so are seeing slower development.
One of Python’s most popular web framework, Django, is not based on asyncio. The latest 3.0 release added support for ASGI, the async counterpart to WSGI. With ASGI you can use daphne or uvicorn to run your application. More async support is planned for 3.1 with asynchronous views and middleware. These are the first steps to supporting asyncio. A fully asynchronous Django would need a complete rewrite of the ORM, cache interface, templates and email. There exists a very big list of everything that needs to be done. Django will be the big push for many into the async world.
The most promising framework based on asyncio is FastAPI. While it is more focused on filling Flask’s old place then it is replacing Django, it is an entirely revolutionary way to create APIs in Python. It makes producing JSON APIs which parse and validate incoming data as simple as using type annotations. In fact the entire framework is based on type annotations, even when you want to specify what resources your view will use. Its also benchmarked as the fastest framework, no doubt thanks to asyncio! FastAPI also allows you to write synchronous views for when you need to fall back to an adapter that hasn’t gone async yet.
Interacting with the database is the one big weakness of async libraries. While the low level adapters, Redis, PostgreSQL, MySQL, etc. have been written, the ORM libraries need work. SqlAlchemy works in some limited capacity thanks to the databases project. However, it still doesn’t give you 100% of the ORM capabilities of SqlAlchemy as statements are returned as record objects instead of models. There exists Sqlalchemy_aio, but that isn’t a true asyncio library (more on that below). A young project named tortoise-orm looks promising, but lacks the advanced query building capabilities of SqlAlchemy or Django. A true asyncio version of SqlAlchemy is needed.
The biggest impediment to adoption are libraries. The async forks of libraries are less maintained then the synchronous counterparts, if they exist at all. Python has been a great language to rely on for obscure tools. Now, it might be a bit complex to use that adapter in the latest web framework. Sometimes async versions of libraries get around the problems by running sync operations in a separate thread. This doesn’t give the best performance. Sqlalchemy_aio has this to say:
It’s not an asyncio implementation of SQLAlchemy or the drivers it uses. sqlalchemy_aio lets you use SQLAlchemy by running operations in a separate thread. […] If performance is critical, perhaps asyncpg can help.
The second biggest hurdle to adoption is that the current solution, gevent, just works. Once you introduce a database query, the latency from the query outweigh the optimizations that asyncio has over gevent. Rewriting all code for such a small benefit isn’t on the top of a lot of peoples mind.
An Async-First Future
Today when building a new python library, developers need to make a choice to use asyncio. In the past, the low adoption meant this wasn’t on a lot of people’s radars. But libraries that consume APIs or use database connections should be written in an async safe way. Attempting to run synchronous code in an asynchronous context is difficult, but the opposite is not. To be future proof, we need to be writing libraries to use asyncio and view current synchronous solutions as deprecated.
To prepare yourself for the Async future, when writing APIs use httpx in asynchronous mode. When you write a library that can use async, use async. If you have to use it from a synchronous context use
asyncio.run(). Django recommends using
from asgiref.sync import async_to_sync to execute async code as another option.
It may take another decade for this next phase of Python to finally be broadly adopted, but mature libraries that top the performance charts show that its doing something right. The future of Python is async and it is fast!