asyncio provides single-threaded concurrency in Python. Not parallelism (that's multiprocessing), but the ability to do something else while waiting for a response (network, file, sleep, etc).
importasyncioasyncdeffetch(url):awaitasyncio.sleep(1)# simulates a network callreturnf"done: {url}"asyncdefmain():results=awaitasyncio.gather(fetch("url1"),fetch("url2"),fetch("url3"),)print(results)asyncio.run(main())# β± ~1 second
When to use asyncio
I/O-bound (network, files, DB) β asyncio is made for this. CPU-bound (heavy computation) β use multiprocessing or concurrent.futures.ProcessPoolExecutor.
The basics
async def and await
async def declares a coroutine. Calling a coroutine doesn't execute it β it returns a coroutine object. You need await to run it:
asyncio.run() creates an event loop, runs the coroutine, then closes the loop. Don't call asyncio.run() from already-async code β use await directly.
Concurrent execution
asyncio.gather() β run multiple coroutines concurrently
asyncdefbackground_job():awaitasyncio.sleep(2)print("background done")asyncdefmain():task=asyncio.create_task(background_job())# do other work while background_job runsprint("doing other work...")awaitasyncio.sleep(1)print("still working...")awaittask# wait for the task to finish
asyncio.TaskGroup β gather with error handling (Python 3.11+)
asyncdefmain():asyncwithasyncio.TaskGroup()astg:task1=tg.create_task(fetch_user(1))task2=tg.create_task(fetch_user(2))task3=tg.create_task(fetch_user(3))# all tasks are done hereprint(task1.result(),task2.result(),task3.result())
TaskGroup vs gather
TaskGroup is safer: if one task raises an exception, the others are automatically cancelled. With gather, by default the others keep running.
asyncdeffetch_limited(sem,session,url):asyncwithsem:# max N tasks at the same timeasyncwithsession.get(url)asresponse:returnawaitresponse.json()asyncdefmain():sem=asyncio.Semaphore(10)# max 10 concurrent requestsasyncwithaiohttp.ClientSession()assession:urls=[f"https://api.example.com/items/{i}"foriinrange(500)]tasks=[fetch_limited(sem,session,url)forurlinurls]results=awaitasyncio.gather(*tasks)asyncio.run(main())
importtime# β Blocks the entire event loopasyncdefbad():time.sleep(5)# nothing else can run for 5s# β Use run_in_executor for blocking code that can't be made asyncasyncdefgood():loop=asyncio.get_event_loop()awaitloop.run_in_executor(None,time.sleep,5)