Description
Hello,
let me provide some context first. PEP 654 introduces a new type of exception - an ExceptionGroup. (Also a BaseExceptionGroup, which is like the BaseException version of it). An ExceptionGroup is essentially a container for a list of inner exceptions. (It also introduces other things, like the except* clause, but I don't think that's relevant to Rich tracebacks.)
The hope is this ExceptionGroup will become a standard way for libraries to raise groups of exceptions. For example, this is a common use case for the Hypothesis library. There will be a backport package on PyPI to provide ExceptionGroups to older Python versions, so third party libraries will be able to use them without issue.
There is another PEP, PEP 678, that builds on ExceptionGroups to reify the concept of an exception note. This PEP has not yet been accepted, and is currently being discussed. (Due to a fluke, the CPython 3.11a4 release contains an implementation of it.) The idea of this PEP is for libraries to be able to attach a string to any exception under the __note__
attribute, and for traceback machinery to simply print it out if it is present. The notes can and will be multiline in practice, I think.
So ExceptionGroups are an official thing, notes will maybe become a thing.
I'm working on a couple of libraries that can benefit from ExceptionGroups so I can give an example. Imagine you're structuring a JSON payload into a class, and there are errors. Setup code:
from attrs import define
from cattrs import structure
@define
class Test:
a: int
b: int
c: float
@define
class Outer:
inner: Test
c: int
structure({"inner": {"a": 'not_an_int', "b": 1}}, Outer)
An ExceptionGroup flies out, and the default traceback module in 3.11 renders it like this:
+ Exception Group Traceback (most recent call last):
| File "/Users/tintvrtkovic/pg/cattrs/a01.py", line 33, in <module>
| structure({"inner": {"a": 'not_an_int', "b": 1}}, Outer)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "/Users/tintvrtkovic/pg/cattrs/src/cattr/converters.py", line 308, in structure
| return self._structure_func.dispatch(cl)(obj, cl)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "<cattrs generated structure __main__.Outer>", line 14, in structure_Outer
| ExceptionGroup
+-+---------------- 1 ----------------
| Exception Group Traceback (most recent call last):
| File "<cattrs generated structure __main__.Outer>", line 5, in structure_Outer
| File "<cattrs generated structure __main__.Test>", line 19, in structure_Test
| ExceptionGroup
| Structuring class attribute Outer.inner
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "<cattrs generated structure __main__.Test>", line 5, in structure_Test
| ValueError: invalid literal for int() with base 10: 'not_an_int'
| Structuring class attribute Test.a
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "<cattrs generated structure __main__.Test>", line 15, in structure_Test
| KeyError: 'c'
| Structuring class attribute Test.c
+------------------------------------
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "<cattrs generated structure __main__.Outer>", line 10, in structure_Outer
| KeyError: 'c'
| Structuring class attribute Outer.c
+------------------------------------
The Structuring class attribute Test.a
strings are notes, added by cattrs.
It's a little messy, but there's a lot of information in there. The Outer
class has errors for attributes inner
(an ExceptionGroup) and c
(a KeyError), and diving in, the Inner class (under inner
) has errors for a
(ValueError) and c
(KeyError).
Anyway, this is how the traceback module prints this. I'm sure a Rich version would be much easier on the eyes ;)