Python 3.11 asyncio TaskGroup vs asyncio.gather()

Python 3.11 was released over two years ago, but I still see a lot of developers ignoring or simply not knowing about asyncio.TaskGroup. Most of us learned to use asyncio.gather() to run multiple async tasks concurrently. It’s what older tutorials teach, and it mostly works—until things go wrong. The issue with gather() is how it handles failures. Imagine you are running three concurrent database queries. If the first query fails and raises an exception, gather() instantly throws that exception back to you. But here is the catch: the other two queries are not cancelled. They keep running in the background as "orphaned" tasks. This leads to: • Wasted CPU, memory, and database connections • Unexpected race conditions later in your application lifecycle You can handle this manually with gather(), but it requires verbose try/except/finally blocks to explicitly track and cancel tasks. TaskGroup fixes this by bringing structured concurrency to Python. It ties the lifetimes of concurrent tasks together using a standard context manager: async with asyncio.TaskGroup() as tg: tg.create_task(fetch_data(1)) tg.create_task(fetch_data(2)) tg.create_task(fetch_data(3)) If any task in that block fails, the TaskGroup automatically cancels all other pending sibling tasks. No orphaned background processes, and no manual cancellation boilerplate. Additionally, if multiple tasks fail at the exact same time, it bundles them into an ExceptionGroup (also introduced in 3.11) so you can see all the errors instead of just the first one. It’s not some massive paradigm shift, just a solid feature that makes handling concurrent async operations cleaner and much safer. If your tasks depend on each other or should fail as a single unit, it's worth making the switch. Are you still using gather(), or have you made the switch to TaskGroup? #Python #Asyncio #SoftwareEngineering #BackendDevelopment

  • graphical user interface, application

I find myself using both, but still reaching for gather() in cases where partial success is acceptable. TaskGroup is great for structured concurrency and “fail-fast as a unit” semantics, but sometimes you don’t want cascading cancellations—you want other tasks to complete even if one fails. In practice, it feels less like a replacement and more about intent: TaskGroup for tightly coupled operations, gather(return_exceptions=True) for independent ones where you can tolerate and inspect failures.

Like
Reply

Yeah, wrong usage of gather() is still pretty common in python dev community

This is something similar to the Kotlin Coroutines Job regulation. Really good point! 👍

Like
Reply

Hm... Interesting feature actually!

See more comments

To view or add a comment, sign in

Explore content categories