Python and async (asyncio)
Python and async (asyncio)
Python is a dynamic scripting language first released in 1991. This post covers Python's flow, the GIL, and how asyncio came about. We also touch on the difference between concurrency and parallelism, where ASGI and FastAPI sit, and alternatives like trio and anyio.
1. About Python
Dutch-born Guido van Rossum started the project on Christmas vacation in 1989 and first released 0.9.0 in February 1991. 1.0 came in 1994, 2.0 in 2000, and 3.0 in 2008. 2.x and 3.x were not compatible, so the language ran on two tracks for a while; official support for 2.7 ended on January 1, 2020.
The language itself is managed by the PSF (Python Software Foundation), and changes go through the PEP (Python Enhancement Proposal) process.
2. The GIL
CPython's interpreter (the reference implementation) holds a GIL so that only one thread runs Python bytecode at a time. The design dates back to around 1992, a tradeoff for single-thread performance and ease of writing C extensions.
Consequences:
- CPU-bound tasks do not parallelize via multithreading (within a single process).
- I/O-bound tasks release the GIL frequently, so multithreading still pays off.
- For real parallelism, separate processes via
multiprocessing.
PEP 703 (2023, Sam Gross) proposed a build mode that disables the GIL on demand, and 3.13 (2024-10) introduced the experimental free-threaded build (--disable-gil). Promotion to standard is happening in stages.
3. Concurrency vs parallelism
| Category | Meaning | Tools |
|---|---|---|
| Concurrency | Structuring multiple tasks to make progress in the same window. Possible on a single core. | asyncio, threading |
| Parallelism | Multiple cores running simultaneously at the same instant. | multiprocessing, C extensions |
Because of the GIL, Python's multithreading helps with concurrency but hits a wall on CPU parallelism. multiprocessing (3.0, 2008) and concurrent.futures (3.2, 2011) fill that gap.
4. The arrival of asyncio
| Version | Event |
|---|---|
| 3.4 (2014) | asyncio added to the standard library. Generator-based @asyncio.coroutine. |
| 3.5 (2015) | PEP 492. The async def and await keywords. |
| 3.6 (2016) | Async generators, async comprehensions. |
| 3.7 (2018) | asyncio.run(), ContextVar. |
| 3.11 (2022) | TaskGroup, except*. |
asyncio cooperatively schedules coroutines on top of an event loop. When a coroutine hits await, it voluntarily yields, and the event loop wakes another ready coroutine:
import asyncio
import httpx
async def fetch(url: str) -> int:
async with httpx.AsyncClient() as c:
r = await c.get(url)
return r.status_code
async def main():
async with asyncio.TaskGroup() as tg: # 3.11+
t1 = tg.create_task(fetch("https://example.com"))
t2 = tg.create_task(fetch("https://www.python.org"))
print(t1.result(), t2.result())
asyncio.run(main())
If one task in a TaskGroup fails, sibling tasks are cancelled automatically and all exceptions are gathered into an ExceptionGroup. This is called "structured concurrency".
5. ASGI's place
WSGI (2003, PEP 333 / 3333) is a synchronous interface. As async frameworks emerged, ASGI (Asynchronous Server Gateway Interface) was defined. Tom Christie shaped it around 2018 while working on Django Channels and Starlette.
- Starlette (Tom Christie, 2018) — a lightweight ASGI framework.
- FastAPI (Sebastián Ramírez, 2018) — built on Starlette, adding Pydantic for typing, validation, and automatic OpenAPI generation.
- Uvicorn (2017) and Hypercorn — ASGI server implementations.
6. Other paths
Different attempts at the async model itself:
| Tool | Started | Trait |
|---|---|---|
| asyncio | 2014, standard | Most widely used. |
| trio | 2017, Nathaniel J. Smith | Designed around structured concurrency from the start. The nursery concept. |
| anyio | 2018, Alex Grönholm | An abstraction layer that runs the same code on top of trio or asyncio. Used internally by Starlette and FastAPI. |
| curio | 2016, David Beazley | Experimental. Explores alternative designs to asyncio. |
asyncio.TaskGroup was influenced by trio's nursery.
Comparison with synchronous frameworks:
| Framework | Model | Note |
|---|---|---|
| Django | WSGI (sync) + ASGI (partial) | 2005. The largest ecosystem. |
| Flask | WSGI (sync) | 2010. The archetypal microframework. |
| FastAPI | ASGI (async) | 2018. Type-hint-driven automatic validation and documentation. |
| Sanic | ASGI (async) | 2016. One of the early async attempts. |
Async is not a silver bullet. If a library or driver does not provide an async interface, it can block the event loop and end up slower. The same goes for CPU-bound work. For these slots use asyncio.to_thread (3.9+) or a separate worker process.
7. Common patterns
import asyncio, httpx
async def fetch_all(urls: list[str]) -> list[int]:
async with httpx.AsyncClient(timeout=10) as c:
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(c.get(u)) for u in urls]
return [t.result().status_code for t in tasks]
8. Common pitfalls
Calling sync code straight from an async function — CPU work or sync DB drivers block the event loop. Wrap them with asyncio.to_thread(fn, *args) or loop.run_in_executor.
time.sleep vs asyncio.sleep — the former halts the entire loop. Inside async functions, use await asyncio.sleep(...).
Missing await — calling f() only creates a coroutine object that never runs. A warning is emitted but easy to miss.
Event loop policies — Windows defaulted to ProactorEventLoop for a long time. Some libraries require SelectorEventLoop instead.
CPU multithreaded code that ignores GIL dependence — spinning up as many threads as cores does not make it faster. You need multiprocessing or a GIL-releasing C extension like NumPy.
CancelledError — asyncio cancellation propagates as an exception. Swallowing it with except Exception defeats cancellation. Re-raising CancelledError in except is the safe pattern.
Closing thoughts
Python's async is a way to route around the GIL using an event loop and cooperative scheduling. The combination of asyncio + FastAPI + Pydantic + httpx is the default modern Python async pair. CPU-bound work still belongs to multiprocessing or C extensions. Once PEP 703's free-threaded build is fully promoted, the picture will shift again.
Next
- rust-for-tauri
- (end of languages)
Python official site, Python Documentation asyncio, PEPs Index, PEP 492 Coroutines with async and await, PEP 703 Making the GIL Optional in CPython, trio, anyio, ASGI spec, and FastAPI are the references.