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.
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)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))