PEP 654 introduced ExceptionGroup and BaseExceptionGroup alongside the except* syntax in Python 3.11. While PEP 654 defined the runtime mechanics and interpreter behavior for exception groups, it did not address docstring conventions. As a result, PEP 257 (Docstring Conventions) and major style guides (such as Google or NumPy style) currently leave developers without a consensus on how to document functions raising them.
Because exception groups fundamentally change how caller code must handle errors, the lack of a standard documentation convention leads to misleading documentation and runtime errors for library users.
The Problem
Consider a function that raises an ExceptionGroup (or BaseExceptionGroup):
def process_batch():
ex1 = ValueError("invalid argument in task 1")
ex2 = TypeError("type mismatch in task 2")
raise ExceptionGroup("batch failure", [ex1, ex2])
# Standard exception catching WILL NOT work:
try:
process_batch()
except (ValueError, TypeError):
print("This block is never reached!") # Unhandled ExceptionGroup!
# Callers MUST use one of these two forms:
try:
process_batch()
except ExceptionGroup:
print("Caught the group generically")
try:
process_batch()
except* (ValueError, TypeError):
print("Handled specific sub-exceptions via except*")
Prior Context
I previously opened issues on popular style guide repositories to discuss this, but neither has reached a resolution (I can’t turn these into URLs due to a maximum hyperlink restriction on new users, sorry):
- Google Python Style Guide Issue #907
- Ruff Issue #16961
- NumPy Docstring Standard Issue #684
Why Existing Docstring Practices Don’t Work
1. Documenting only the inner exceptions
def process_batch():
"""
Raises:
ValueError: If task 1 fails.
TypeError: If task 2 fails.
"""
- Issue: This strongly implies that standard
try ... except ValueErrorblocks will work. Callers reading this might write code that crashes at runtime when anExceptionGroupis raised instead.
2. Documenting only ExceptionGroup
def process_batch():
"""
Raises:
ExceptionGroup: If any task in the batch fails.
"""
- Issue: This hides the inner exceptions entirely. Callers who want to handle specific error cases via
except* ValueErrorhave no way of knowing what sub-exceptions can occur without reading the underlying source code, defeating the purpose of docstrings.
Proposed Conventions for Discussion
To clearly signal both that an exception group is raised and what sub-exceptions it contains, here are four potential approaches I’ve come up with:
Option A: Generic Type Notation
Utilize typing-like syntax to denote contained types:
def process_batch():
"""
Raises:
ExceptionGroup[ValueError, TypeError]: Raised when batch tasks fail.
Contains details on argument or type mismatches.
"""
- Pros: Highly concise, aligns naturally with Python typing syntax.
- Cons: Harder to write individual prose descriptions for each sub-exception type.
Option B: Hierarchical Structure
Use a nested block structure under ExceptionGroup:
def process_batch():
"""
Raises:
ExceptionGroup: Raised if one or more batch tasks fail.
ValueError: If task arguments are invalid.
TypeError: If task types are mismatched.
"""
- Pros: Clean, highly readable, allows per-exception descriptions.
- Cons: May require updates to docstring parsers (like Sphinx/Napoleon or Griffe) to avoid misinterpreting nested items as list continuation text; verbose.
Option C: Syntax-Mirroring (*Exception)
Use an asterisk prefix to visually match Python’s except* syntax:
def process_batch():
"""
Raises:
*ValueError: Raised inside an ExceptionGroup if task arguments are invalid.
*TypeError: Raised inside an ExceptionGroup if task types mismatch.
"""
- Pros: Explicitly informs the reader that
except*is required to catch it. - Cons: Doesn’t explicitly state the parent
ExceptionGrouporBaseExceptionGrouptype; unconventional markup syntax.
Option D: Explicit Qualifier Syntax
def process_batch():
"""
Raises:
ValueError (via ExceptionGroup): If task arguments are invalid.
TypeError (via ExceptionGroup): If task types are mismatched.
"""
- Pros: Makes it trivial to adapt docstring parsers to recognize, without requiring actual parser changes.
- Cons: Verbose when multiple exceptions are involved; highly unconventional markup syntax.
Relevant Questions to be Answered
- From an AST parsing and rendering perspective, which of these options (or another alternative) would be the easiest and cleanest to support?
- How are devs currently documenting functions that raise
ExceptionGroupor useanyio/asynciotaskgroups? - Is there interest in establishing an consensus/guideline so docstring tools don’t fragment on this?
I’d love to hear thoughts, alternative proposals, or preferences! Thank you for your time.