Python

Python Exception Groups👨‍💻

Before Python 3.11, exception handling had a fundamental limitation: you could only propagate one exception at a time. If three concurrent tasks failed simultaneously, you had to pick one error and throw away the rest, or stuff them into a custom wrapper. Exception groups fix this by making multiple simultaneous errors a first-class concept in the language.

ExceptionGroup is a built-in exception type that wraps a list of exceptions under a single message. The companion except* syntax (pronounced "except star") lets you pattern-match against the contents of a group, handling different error types in separate clauses while automatically re-raising anything you didn't handle. This is not just a concurrency feature. Any situation where multiple independent operations can fail — form validation, batch processing, parallel I/O — benefits from being able to report all failures at once instead of stopping at the first one.

Key Takeaways

  • 1`ExceptionGroup` (Python 3.11+) wraps multiple exceptions into a single raiseable object — each with its own type, message, and traceback preserved intact
  • 2The `except*` clause matches exceptions inside a group by type and handles them in isolation; unmatched exceptions are automatically re-raised in a new group
  • 3`BaseExceptionGroup` is the parent class that can hold any exception including `KeyboardInterrupt` and `SystemExit`; `ExceptionGroup` only accepts `Exception` subclasses
  • 4You cannot mix `except` and `except*` in the same `try` block — pick one style per block
  • 5The `split()` and `subgroup()` methods let you partition an exception group by type or predicate without writing loops, which is essential for retry logic and error reporting
  • 6`asyncio.TaskGroup` raises an `ExceptionGroup` when multiple tasks fail, making `except*` the natural way to handle structured concurrency errors

Master python exception groups

Take the Python Fundamentals course with hands-on lessons and challenges.

Examples

Batch validation — collect all errors, report them together

python

Without exception groups you'd return a list of error strings or raise on the first failure. ExceptionGroup lets you use standard exception handling (try/except*) while reporting every problem at once — the caller decides how to present them.

TaskGroup concurrent failures — except* routes errors by type

python

When multiple HTTP requests fail in a TaskGroup, the errors arrive bundled in an ExceptionGroup. Each except* clause handles one category independently — HTTP errors get logged with status codes, network errors get a different message. Successful results are still available on completed tasks.

split() and subgroup() — partition errors for retry logic

python

The split() method returns a tuple of (matching_group, rest_group) — either can be None if nothing matched. This is the cleanest way to implement retry logic: separate transient failures from permanent ones, re-run only the transient batch, and propagate permanent failures immediately.

BaseExceptionGroup vs ExceptionGroup — when the distinction matters

python

In practice you almost always use ExceptionGroup. BaseExceptionGroup exists because TaskGroup needs to handle cases where KeyboardInterrupt or SystemExit happens during concurrent execution. The constructor auto-selects the right type based on what you put in it, so you rarely need to think about this unless you're building a framework.

Nested exception groups — structured error hierarchies

python

Exception groups can nest. Each sync_user call collects its own failures into a group, and when TaskGroup collects those per-user groups, you get a tree of errors. The except* clauses match at any depth, so you can handle ConnectionError at the top level even if some are nested inside per-user groups.

Common Mistakes

Mistake:

Mixing `except` and `except*` in the same `try` block — this is a `SyntaxError`

Fix:

Choose one style per `try` block. If you need to handle both grouped and non-grouped exceptions, either use `except*` for everything (it works even when only one exception matches) or wrap the `except*` block inside a regular `try/except`.

Mistake:

Assuming `except*` catches the exception directly — it actually catches an `ExceptionGroup` containing matches, so `eg` is always a group, never a bare exception

Fix:

Always iterate `eg.exceptions` to access the individual exceptions. Even if only one exception matched, `eg.exceptions` is a tuple of length 1, not the exception itself.

Mistake:

Silently swallowing exceptions by catching `except* Exception` without re-raising unhandled ones

Fix:

Be specific with your `except*` clauses. Unlike regular `except`, unmatched exceptions inside the group are automatically re-raised — but if you catch `Exception`, everything matches and nothing propagates. Catch only the types you actually handle.

Mistake:

Trying to raise `ExceptionGroup` with an empty list — `ExceptionGroup("msg", [])` raises a `ValueError`

Fix:

Guard with `if errors: raise ExceptionGroup(...)`. An exception group must contain at least one exception. This is a deliberate design choice to prevent meaningless empty groups from propagating.

Best Practices

  • Use exception groups for batch operations (validation, data import, multi-step sync) where reporting all failures is better than stopping at the first one
  • Keep `except*` clauses specific — catch `ValueError` and `TypeError` separately rather than a broad `Exception`, so unhandled errors propagate naturally
  • Use `split()` for control flow decisions (retry vs. fail) and `subgroup()` for inspection or logging where you only need the matching subset
  • When building libraries that raise exception groups, document it clearly — callers need to know that `except*` is required and which exception types to expect inside the group
  • Prefer `ExceptionGroup` over `BaseExceptionGroup` unless you're writing framework code that genuinely needs to wrap `KeyboardInterrupt` or `SystemExit`

Summary

Exception groups solve a real gap in Python's error model: handling multiple simultaneous failures without losing information. ExceptionGroup wraps a list of exceptions, and except* pattern-matches against their types — each clause runs independently, and unhandled exceptions re-raise automatically. Combined with asyncio.TaskGroup for structured concurrency, this gives you clean error handling for concurrent code without manual bookkeeping. The split() and subgroup() methods make retry logic and error routing straightforward. Available since Python 3.11, with a backport (exceptiongroup package) for 3.10 and earlier.

Practice Python with hands-on challenges

Learn python exception groups hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master Python with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.