Skip to main content
🪺
PEP 654 · Speedrun · Python 3.11 (2022)

PEP 654 in 90 seconds — except* and the death of "only first error shown"

Concurrent failures used to hide each other. Not any more.

Python 3.11 introduced exception **groups** — a single exception that wraps many. The motivation came from `asyncio.TaskGroup`: if three tasks fail at once, you can't pick one to re-raise. PEP 654 says: re-raise all of them, in a group.

To handle a group you use a new syntax — `except*`. Unlike `except` (one branch matches once), `except*` walks the group and runs your handler for **every** matching sub-exception, then re-raises any unmatched ones as a smaller group. Small syntactic change, big async story: every `asyncio.TaskGroup` user now sees all failures at once instead of guessing which one to fix first.

Before — Python ≤ 3.10, gather hides errors
import asyncio

async def main():
    try:
        await asyncio.gather(broken_a(), broken_b(), broken_c())
    except ValueError as e:
        # Only the FIRST raised exception surfaces here.
        # broken_b and broken_c errors? Lost. asyncio just prints
        # "Task exception was never retrieved" to stderr.
        print(e)
After — Python 3.11+, TaskGroup + except*
import asyncio

async def main():
    try:
        async with asyncio.TaskGroup() as tg:
            tg.create_task(broken_a())  # raises ValueError("a")
            tg.create_task(broken_b())  # raises TypeError("b")
            tg.create_task(broken_c())  # raises ValueError("c")
    except* ValueError as eg:
        print("values:", [str(e) for e in eg.exceptions])
    except* TypeError as eg:
        print("types:", [type(e).__name__ for e in eg.exceptions])

🎯 Predict the output

What does this print? (Hint: `except*` partitions the group by type — each branch handles every matching error in the group.)

# Synthesised group — same shape TaskGroup produces.
eg = ExceptionGroup("oops", [
    ValueError("a"),
    TypeError("b"),
    ValueError("c"),
])

try:
    raise eg
except* ValueError as g:
    print("values:", len(g.exceptions))
except* TypeError as g:
    print("types:", len(g.exceptions))
Async & error handling → Foundations track

Or speedrun another PEP

PEP 572Assignment as an expression — 3 lines become 1.
PEP 572 in 90 seconds — the walrus that ate your for-loop
PEP 484Python stays dynamic — but you can opt into typing.
PEP 484 in 90 seconds — type hints, without TypeScript-envy
PEP 622Switch statements are a 5% feature. Pattern matching is the 95%.
PEP 622 in 90 seconds — match / case isn't just a switch