Some conclusions from playing with exception groups and except* by Łukasz Langa ( https://fediverse.org/llanga )

Introduction

This was a week where I tried something new. Instead of cutting through tens of shallow PRs, I focused deeply on a single one.

I installed Irit Katriel’s GH-29581 to try out PEP 654’s new except* and ExceptionGroup objects.

Let’s talk about what I found out. We’ll start with a short reminder of how exceptions work today, and then I’ll talk some about asyncio and how except* makes everything so much better (spoiler alert!).

Enter except* !

Finally, after a 2,000 word buildup, we reach the actual meat of the post: exception groups !

An exception group is a special subclass of Exception that looks something like this:

>>> eg = ExceptionGroup(
...   "Server process exceptions",
...   (
...     ValueError("invalid logger config"),
...     TypeError("bytes expected as payload"),
...     ExceptionGroup(
...       "Fetching current configuration",
...       (
...         ValueError("timeout can't be negative"),
...         asyncio.TimeoutError("task-03 timed out"),
...       ),
...     ),
...   ),
... )

Some conclusions from playing with exception groups and except*

This is still very new functionality – the PR isn’t even landed yet – but after spending a week with it, I can already tell you that:

  • exception groups compose very well with the rest of the language, you can mostly treat them like regular exceptions in outer try:except: blocks where you only retry or log errors;

  • try:except*: is a big convenience for when you need to deal with contents of an exception group in a readable way – but it isn’t “viral”, you don’t have to convert your existing try:except: blocks, you won’t have to teach it to third-graders in their first week of Python (just like you’re not teaching them about **kwargs);

  • the except* keyword isn’t a single keyword – the star is a separate token – but the authors recommend this spelling and not except Exception because in the presence of multiple exception types it looks confusing (except *(SomeError, OtherError) is worse than except (SomeError, OtherError);

  • it’s not only for asyncio! I can easily imagine other frameworks that will use this functionality: multiprocessing, concurrent.futures, atexit, and so on.

The future of asyncio error handling !

Remember our wordy asyncio.wait example along with its necessary pending task cancellation that runs them to completion, and the ugly loop to retrieve all exceptions one by one ?

That was a form of resource tracking so an obvious candidate for it was to use a with statement, right ?

How about something like this ?

async def download_many():
async with TaskGroup(name=”Downloads”) as tg:
for coro in (

get_one(” http://example.com/dl=f1 ”, “f1.zip”), get_one(” http://example.com/dl=f2 ”, “f2.zip”), get_one(” http://example.com/dl=f3 ”, “f3.zip”),

):

tg.create_task(coro)

This code is the equivalent of the task handling we had before.

It always waits for all tasks to finish, it always cancels things properly, and always gathers multiple exceptions in a group so you can handle them like this in an outer call:

try:
    await download_many()
except* asyncio.TimeoutError:
    ...
except* aiohttp.ClientSSLError:
    ...
except* aiohttp.ClientResponseError:
    ...