AI Skill Report Card
Handling Exception Groups
Quick Start
Python# Raising multiple exceptions at once try: raise ExceptionGroup("multiple failures", [ ValueError("bad value"), TypeError("bad type"), ]) except* ValueError as eg: print(f"Caught ValueErrors: {eg.exceptions}") except* TypeError as eg: print(f"Caught TypeErrors: {eg.exceptions}")
except* matches by exception type against each leaf exception in the group, splits the group into "matched" and "unmatched" subgroups, and re-raises any unmatched portion after all clauses run.
Workflow13 / 15
Progress:
- Step 1: Identify whether errors arise from concurrent/parallel operations (asyncio tasks, thread pools, batch validation) — these are prime candidates for ExceptionGroup
- Step 2: Decide grouping strategy — one flat group, or nested groups preserving sub-task boundaries
- Step 3: Construct the group with
ExceptionGroup(msg, [exc1, exc2, ...])orBaseExceptionGroupif any exception is aBaseException(e.g.,KeyboardInterrupt,SystemExit) - Step 4: Handle with
except*clauses ordered from most specific to most general type - Step 5: Verify unmatched exceptions propagate correctly (don't swallow silently)
- Step 6: Test with
.split()/.subgroup()if you need programmatic filtering instead ofexcept*
Recommendation▾
Add a brief example showing what happens when except* leaves unmatched exceptions propagating (concrete traceback output) to reinforce the pitfall.
Examples18 / 20
Example 1: Basic construction and type selection
Input:
Pythonexcs = [ValueError("v1"), ValueError("v2")] KeyboardInterrupt # one of the exceptions is a BaseException, not Exception
Output:
Python# Because KeyboardInterrupt is not an Exception, must use BaseExceptionGroup eg = BaseExceptionGroup("mixed", [ValueError("v1"), KeyboardInterrupt()]) # ExceptionGroup() would raise TypeError here since it requires all Exception subclasses
Example 2: Nested groups from concurrent tasks
Input:
Pythonimport asyncio async def worker(n): if n % 2 == 0: raise ValueError(f"even fail {n}") raise TypeError(f"odd fail {n}") async def main(): async with asyncio.TaskGroup() as tg: for i in range(4): tg.create_task(worker(i))
Output:
Pythontry: asyncio.run(main()) except* ValueError as eg: print("ValueErrors:", [str(e) for e in eg.exceptions]) except* TypeError as eg: print("TypeErrors:", [str(e) for e in eg.exceptions]) # TaskGroup automatically collects all task failures into one ExceptionGroup
Example 3: Programmatic filtering without except*
Input:
Pythoneg = ExceptionGroup("errors", [ValueError("a"), TypeError("b"), ValueError("c")])
Output:
Pythonvalue_errors, rest = eg.split(ValueError) # value_errors: ExceptionGroup("errors", [ValueError("a"), ValueError("c")]) # rest: ExceptionGroup("errors", [TypeError("b")])
Recommendation▾
Include a short note on Python version requirement (3.11+) since except* syntax is version-gated, which is a critical practical constraint.
Best Practices
- Use
except*(notexcept) whenever you're catching from a block that may raiseExceptionGroup/BaseExceptionGroup— mixing them raisesSyntaxError. - Choose
BaseExceptionGrouponly when necessary (contains aBaseException); preferExceptionGroupfor normal error handling since it's a subclass and satisfies both. - Order
except*clauses from specific to general — like regularexcept, but each clause can still match if the group contains a mix of types. - Preserve nesting from source tasks/operations rather than flattening, so
.exceptionsretains context (e.g., which task failed). - Use
traceback.print_exception(eg)for readable nested tracebacks — the standard traceback module has explicit support for exception groups. - Attach
__notes__viaadd_note()for extra context on individual exceptions before grouping, rather than stuffing details into the group message.
Common Pitfalls
- Don't mix bare
exceptandexcept*in the sametrystatement — it's aSyntaxError. - Don't assume
except*clause runs once per matching exception type only — it collects all matching leaf exceptions from the (possibly nested) group into one subgroup and runs the handler once. - Don't forget that unhandled/unmatched exceptions inside the group still propagate — an
except*block that doesn't cover all types will re-raise a new group with the leftovers. - Don't call
ExceptionGroup(msg, [])— an empty exceptions list raisesValueError; a group must always contain at least one exception. - Don't use
isinstance(e, ExceptionGroup)checks inside a plainexceptand manually iterate.exceptionswhenexcept*already does this correctly, including nested groups.