ExceptionGroup
Several errors at once, in one exception: except* lets each handler take the members of its type, and anything unhandled is re-raised.
Demo
def parse_all(values): nums, errors = [], [] for v in values: try: nums.append(int(v)) except ValueError as e: errors.append(e) if errors: raise ExceptionGroup('bad values', errors) return nums try: r = parse_all(['1', '2', '3']) except ExceptionGroup as eg: r = (str(eg), eg.exceptions) r
Every bad value is reported, not just the first — that is the point of a group. str(eg) only counts the members ('bad values (2 sub-exceptions)'); the details live in eg.exceptions. Uncaught, a group prints a traceback with one numbered section per member. In Raise, try n = 0: a group must contain at least one exception.
Constructor
| Name | Type | Required | Description |
|---|---|---|---|
| msg | str | yes | The group message, available as eg.message. Must be a str. |
| excs | Sequence[Exception] | yes | Non-empty sequence of exception instances, stored as the tuple eg.exceptions. ExceptionGroup accepts only Exception subclasses; use BaseExceptionGroup for others. |
Attributes
| Attribute | Type | Meaning |
|---|---|---|
| message | str | The msg argument. Read-only. |
| exceptions | tuple | The member exceptions (may include nested groups). Read-only. |
| subgroup(condition) | method | A new group with only the matching members (a type, a tuple of types, or since 3.13 any callable), or None if nothing matches. |
| split(condition) | method | (match, rest) — subgroup(condition) plus a group of the non-matching members; either side may be None. |
| derive(excs) | method | Same message, new members. Override it in subclasses so subgroup()/split() build your class. |
Common patterns
errors = [] for field, value in form.items(): try: validate(field, value) except ValueError as e: e.add_note(f'field: {field}') errors.append(e) if errors: raise ExceptionGroup('invalid form', errors)
try: run_all() except* TimeoutError as eg: retry_later(len(eg.exceptions)) except* ConnectionError as eg: for e in eg.exceptions: log.warning('connection failed: %s', e)
async def main(): try: async with asyncio.TaskGroup() as tg: for url in urls: tg.create_task(fetch(url)) except* OSError as eg: print(f'{len(eg.exceptions)} downloads failed')
Examples
Pitfalls
try: raise ExceptionGroup('batch', [ValueError('bad')]) except ValueError: r = 'handled' r
try: raise ExceptionGroup('batch', [ValueError('bad')]) except* ValueError: r = 'handled' r
try: pass except* ValueError: pass except TypeError: pass
try: raise ExceptionGroup('m', [TypeError('t')]) except* ValueError: r = 'value' except* TypeError: r = 'type' r
ExceptionGroup('stop', [KeyboardInterrupt()])
BaseExceptionGroup('stop', [KeyboardInterrupt()])
try: raise ExceptionGroup('m', [ValueError()]) except* ExceptionGroup: r = 'handled' r
try: raise ExceptionGroup('m', [ValueError()]) except ExceptionGroup as eg: r = len(eg.exceptions) r
When to use
- Concurrency: several tasks can fail at once (asyncio.TaskGroup, your own worker pools)
- Validation that should report every problem, not just the first
- Cleanup code where more than one step can fail and none should be lost
- Only one thing can fail → raise that exception directly
- Code that must run on Python 3.10 or older → the exceptiongroup backport or a list attribute on your own exception
- Just adding context to one error → add_note() or raise ... from e
Notes
FAQ
except* (Python 3.11+) handles exception groups. except* ValueError as eg matches the ValueErrors inside a group; eg is a new ExceptionGroup containing just those. Every except* clause whose type matches runs once, and members no clause handled are re-raised in a group after the try statement.