From b1afcb8806ae481f5c5bb13c00111b282dc0775c Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Wed, 19 Nov 2025 16:27:47 +1030 Subject: [PATCH 01/41] Use `self.exceptions` in `BaseExceptionGroup` repr by supporting custom reprs in `ComplexExtendsException` --- Objects/exceptions.c | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 244d8f39e2bae5..0dd7e4c4a2125c 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -694,12 +694,12 @@ PyTypeObject _PyExc_ ## EXCNAME = { \ #define ComplexExtendsException(EXCBASE, EXCNAME, EXCSTORE, EXCNEW, \ EXCMETHODS, EXCMEMBERS, EXCGETSET, \ - EXCSTR, EXCDOC) \ + EXCSTR, EXCREPR, EXCDOC) \ static PyTypeObject _PyExc_ ## EXCNAME = { \ PyVarObject_HEAD_INIT(NULL, 0) \ # EXCNAME, \ sizeof(Py ## EXCSTORE ## Object), 0, \ - EXCSTORE ## _dealloc, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, \ + EXCSTORE ## _dealloc, 0, 0, 0, 0, EXCREPR, 0, 0, 0, 0, 0, \ EXCSTR, 0, 0, 0, \ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, \ PyDoc_STR(EXCDOC), EXCSTORE ## _traverse, \ @@ -792,7 +792,7 @@ StopIteration_traverse(PyObject *op, visitproc visit, void *arg) } ComplexExtendsException(PyExc_Exception, StopIteration, StopIteration, - 0, 0, StopIteration_members, 0, 0, + 0, 0, StopIteration_members, 0, 0, 0, "Signal the end from iterator.__next__()."); @@ -865,7 +865,7 @@ static PyMemberDef SystemExit_members[] = { }; ComplexExtendsException(PyExc_BaseException, SystemExit, SystemExit, - 0, 0, SystemExit_members, 0, 0, + 0, 0, SystemExit_members, 0, 0, 0, "Request to exit from the interpreter."); /* @@ -1063,6 +1063,19 @@ BaseExceptionGroup_str(PyObject *op) self->msg, num_excs, num_excs > 1 ? "s" : ""); } +static PyObject * +BaseExceptionGroup_repr(PyObject *op) +{ + PyBaseExceptionGroupObject *self = PyBaseExceptionGroupObject_CAST(op); + assert(self->msg); + assert(self->excs); + + const char *name = _PyType_Name(Py_TYPE(self)); + return PyUnicode_FromFormat( + "%s(%R, %R)", name, + self->msg, self->excs); +} + /*[clinic input] @critical_section BaseExceptionGroup.derive @@ -1697,7 +1710,7 @@ static PyMethodDef BaseExceptionGroup_methods[] = { ComplexExtendsException(PyExc_BaseException, BaseExceptionGroup, BaseExceptionGroup, BaseExceptionGroup_new /* new */, BaseExceptionGroup_methods, BaseExceptionGroup_members, - 0 /* getset */, BaseExceptionGroup_str, + 0 /* getset */, BaseExceptionGroup_str, BaseExceptionGroup_repr, "A combination of multiple unrelated exceptions."); /* @@ -2425,7 +2438,7 @@ static PyGetSetDef OSError_getset[] = { ComplexExtendsException(PyExc_Exception, OSError, OSError, OSError_new, OSError_methods, OSError_members, OSError_getset, - OSError_str, + OSError_str, 0, "Base class for I/O related errors."); @@ -2566,7 +2579,7 @@ static PyMethodDef NameError_methods[] = { ComplexExtendsException(PyExc_Exception, NameError, NameError, 0, NameError_methods, NameError_members, - 0, BaseException_str, "Name not found globally."); + 0, BaseException_str, 0, "Name not found globally."); /* * UnboundLocalError extends NameError @@ -2700,7 +2713,7 @@ static PyMethodDef AttributeError_methods[] = { ComplexExtendsException(PyExc_Exception, AttributeError, AttributeError, 0, AttributeError_methods, AttributeError_members, - 0, BaseException_str, "Attribute not found."); + 0, BaseException_str, 0, "Attribute not found."); /* * SyntaxError extends Exception @@ -2899,7 +2912,7 @@ static PyMemberDef SyntaxError_members[] = { ComplexExtendsException(PyExc_Exception, SyntaxError, SyntaxError, 0, 0, SyntaxError_members, 0, - SyntaxError_str, "Invalid syntax."); + SyntaxError_str, 0, "Invalid syntax."); /* @@ -2959,7 +2972,7 @@ KeyError_str(PyObject *op) } ComplexExtendsException(PyExc_LookupError, KeyError, BaseException, - 0, 0, 0, 0, KeyError_str, "Mapping key not found."); + 0, 0, 0, 0, KeyError_str, 0, "Mapping key not found."); /* From 59bc3c790b0a05302f94a53361c2a5e636f68a15 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Wed, 19 Nov 2025 16:28:23 +1030 Subject: [PATCH 02/41] Update `ExceptionGroup` tests for new repr and add mutation test --- Lib/test/test_exception_group.py | 45 +++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 5df2c41c6b56bc..c4cbc36d2fb7de 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -131,12 +131,17 @@ class MyBEG(BaseExceptionGroup): class StrAndReprTests(unittest.TestCase): def test_ExceptionGroup(self): + eg_excs = [ValueError(1), TypeError(2)] eg = BaseExceptionGroup( - 'flat', [ValueError(1), TypeError(2)]) + 'flat', eg_excs) self.assertEqual(str(eg), "flat (2 sub-exceptions)") self.assertEqual(repr(eg), - "ExceptionGroup('flat', [ValueError(1), TypeError(2)])") + "ExceptionGroup('flat', (ValueError(1), TypeError(2)))") + + # Mutate the list of exceptions passed to BaseExceptionGroup. + # This shouldn't change the EG's functionality, nor its repr. + eg_excs.clear() eg = BaseExceptionGroup( 'nested', [eg, ValueError(1), eg, TypeError(2)]) @@ -144,21 +149,26 @@ def test_ExceptionGroup(self): self.assertEqual(str(eg), "nested (4 sub-exceptions)") self.assertEqual(repr(eg), "ExceptionGroup('nested', " - "[ExceptionGroup('flat', " - "[ValueError(1), TypeError(2)]), " + "(ExceptionGroup('flat', " + "(ValueError(1), TypeError(2))), " "ValueError(1), " "ExceptionGroup('flat', " - "[ValueError(1), TypeError(2)]), TypeError(2)])") + "(ValueError(1), TypeError(2))), TypeError(2)))") def test_BaseExceptionGroup(self): + eg_excs = [ValueError(1), KeyboardInterrupt(2)] eg = BaseExceptionGroup( - 'flat', [ValueError(1), KeyboardInterrupt(2)]) + 'flat', eg_excs) self.assertEqual(str(eg), "flat (2 sub-exceptions)") self.assertEqual(repr(eg), "BaseExceptionGroup(" "'flat', " - "[ValueError(1), KeyboardInterrupt(2)])") + "(ValueError(1), KeyboardInterrupt(2)))") + + # Mutate the list of exceptions passed to BaseExceptionGroup. + # This shouldn't change the EG's functionality, nor its repr. + eg_excs.clear() eg = BaseExceptionGroup( 'nested', [eg, ValueError(1), eg]) @@ -166,21 +176,26 @@ def test_BaseExceptionGroup(self): self.assertEqual(str(eg), "nested (3 sub-exceptions)") self.assertEqual(repr(eg), "BaseExceptionGroup('nested', " - "[BaseExceptionGroup('flat', " - "[ValueError(1), KeyboardInterrupt(2)]), " + "(BaseExceptionGroup('flat', " + "(ValueError(1), KeyboardInterrupt(2))), " "ValueError(1), " "BaseExceptionGroup('flat', " - "[ValueError(1), KeyboardInterrupt(2)])])") + "(ValueError(1), KeyboardInterrupt(2)))))") def test_custom_exception(self): class MyEG(ExceptionGroup): pass + eg_excs = [ValueError(1), TypeError(2)] eg = MyEG( - 'flat', [ValueError(1), TypeError(2)]) + 'flat', eg_excs) self.assertEqual(str(eg), "flat (2 sub-exceptions)") - self.assertEqual(repr(eg), "MyEG('flat', [ValueError(1), TypeError(2)])") + self.assertEqual(repr(eg), "MyEG('flat', (ValueError(1), TypeError(2)))") + + # Mutate the list of exceptions passed to BaseExceptionGroup. + # This shouldn't change the EG's functionality, nor its repr. + eg_excs.clear() eg = MyEG( 'nested', [eg, ValueError(1), eg, TypeError(2)]) @@ -188,10 +203,10 @@ class MyEG(ExceptionGroup): self.assertEqual(str(eg), "nested (4 sub-exceptions)") self.assertEqual(repr(eg), ( "MyEG('nested', " - "[MyEG('flat', [ValueError(1), TypeError(2)]), " + "(MyEG('flat', (ValueError(1), TypeError(2))), " "ValueError(1), " - "MyEG('flat', [ValueError(1), TypeError(2)]), " - "TypeError(2)])")) + "MyEG('flat', (ValueError(1), TypeError(2))), " + "TypeError(2)))")) def create_simple_eg(): From ca2d21da242bfec684109d03923a7d063f866095 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Wed, 19 Nov 2025 16:30:19 +1030 Subject: [PATCH 03/41] Fix typo in test comment --- Lib/test/test_exception_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index c4cbc36d2fb7de..b8ca7935c2618d 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -193,7 +193,7 @@ class MyEG(ExceptionGroup): self.assertEqual(str(eg), "flat (2 sub-exceptions)") self.assertEqual(repr(eg), "MyEG('flat', (ValueError(1), TypeError(2)))") - # Mutate the list of exceptions passed to BaseExceptionGroup. + # Mutate the list of exceptions passed to MyEG. # This shouldn't change the EG's functionality, nor its repr. eg_excs.clear() From 493ab01143fe412892781704d0127d120f064092 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Wed, 19 Nov 2025 16:35:13 +1030 Subject: [PATCH 04/41] Update docs for `BaseExceptionGroup` repr change --- Doc/library/exceptions.rst | 6 +++--- Doc/reference/compound_stmts.rst | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/library/exceptions.rst b/Doc/library/exceptions.rst index 16d42c010f6df0..22715c979ab8f2 100644 --- a/Doc/library/exceptions.rst +++ b/Doc/library/exceptions.rst @@ -1053,11 +1053,11 @@ their subgroups based on the types of the contained exceptions. ... >>> match, rest = exc.split(ValueError) >>> exc, exc.__context__, exc.__cause__, exc.__notes__ - (MyGroup('eg', [ValueError(1), TypeError(2)]), Exception('context'), Exception('cause'), ['a note']) + (MyGroup('eg', (ValueError(1), TypeError(2))), Exception('context'), Exception('cause'), ['a note']) >>> match, match.__context__, match.__cause__, match.__notes__ - (MyGroup('eg', [ValueError(1)]), Exception('context'), Exception('cause'), ['a note']) + (MyGroup('eg', (ValueError(1),)), Exception('context'), Exception('cause'), ['a note']) >>> rest, rest.__context__, rest.__cause__, rest.__notes__ - (MyGroup('eg', [TypeError(2)]), Exception('context'), Exception('cause'), ['a note']) + (MyGroup('eg', (TypeError(2),)), Exception('context'), Exception('cause'), ['a note']) >>> exc.__traceback__ is match.__traceback__ is rest.__traceback__ True diff --git a/Doc/reference/compound_stmts.rst b/Doc/reference/compound_stmts.rst index 6521b4bee50758..861c221502ed4d 100644 --- a/Doc/reference/compound_stmts.rst +++ b/Doc/reference/compound_stmts.rst @@ -388,7 +388,7 @@ type of the target ``e`` is consistently :exc:`BaseExceptionGroup`:: ... except* BlockingIOError as e: ... print(repr(e)) ... - ExceptionGroup('', (BlockingIOError())) + ExceptionGroup('', (BlockingIOError(),)) :keyword:`break`, :keyword:`continue` and :keyword:`return` cannot appear in an :keyword:`!except*` clause. From cd90d51c63fb4d6e7e3bf91fe8b439f06400c57d Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Wed, 19 Nov 2025 16:40:27 +1030 Subject: [PATCH 05/41] Add NEWS entry --- .../2025-11-19-16-40-24.gh-issue-141732.PTetqp.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-16-40-24.gh-issue-141732.PTetqp.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-16-40-24.gh-issue-141732.PTetqp.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-16-40-24.gh-issue-141732.PTetqp.rst new file mode 100644 index 00000000000000..17848e65bac76c --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-16-40-24.gh-issue-141732.PTetqp.rst @@ -0,0 +1,2 @@ +Ensure the repr for :exc:`ExceptionGroup` and :exc:`BaseExceptionGroup` does +not change when its original exception list is mutated. From 177b5355281af76c495c952f9720f68aae228f06 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Wed, 19 Nov 2025 17:06:45 +1030 Subject: [PATCH 06/41] Update `ExceptionGroup` repr in `test_traceback` --- Lib/test/test_traceback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index bf57867a8715c0..921be6374fe810 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -5170,7 +5170,7 @@ def expected(t, m, fn, l, f, E, e, z): f' | File {fn}"{__file__}"{z}, line {l}{lno_foo+9}{z}, in {f}test_colorized_traceback_from_exception_group{z}', f' | {e}foo{z}{E}(){z}', f' | {e}~~~{z}{E}^^{z}', - f" | e = ExceptionGroup('test', [ZeroDivisionError('division by zero')])", + f" | e = ExceptionGroup('test', (ZeroDivisionError('division by zero'),))", f" | foo = {foo}", f' | self = <{__name__}.TestColorizedTraceback testMethod=test_colorized_traceback_from_exception_group>', f' | File {fn}"{__file__}"{z}, line {l}{lno_foo+6}{z}, in {f}foo{z}', From af4f086abb9a5c19203b564e8e1b19965d2fbd6a Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Fri, 21 Nov 2025 18:26:52 +1030 Subject: [PATCH 07/41] Revert "Update `ExceptionGroup` repr in `test_traceback`" This reverts commit 177b5355281af76c495c952f9720f68aae228f06. --- Lib/test/test_traceback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 921be6374fe810..bf57867a8715c0 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -5170,7 +5170,7 @@ def expected(t, m, fn, l, f, E, e, z): f' | File {fn}"{__file__}"{z}, line {l}{lno_foo+9}{z}, in {f}test_colorized_traceback_from_exception_group{z}', f' | {e}foo{z}{E}(){z}', f' | {e}~~~{z}{E}^^{z}', - f" | e = ExceptionGroup('test', (ZeroDivisionError('division by zero'),))", + f" | e = ExceptionGroup('test', [ZeroDivisionError('division by zero')])", f" | foo = {foo}", f' | self = <{__name__}.TestColorizedTraceback testMethod=test_colorized_traceback_from_exception_group>', f' | File {fn}"{__file__}"{z}, line {l}{lno_foo+6}{z}, in {f}foo{z}', From 72918ead960e643aa76bfb43b27fa6a8fc1a9911 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Fri, 21 Nov 2025 18:29:47 +1030 Subject: [PATCH 08/41] Update `ExceptionGroup` tests to no longer change the repr --- Lib/test/test_exception_group.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index b8ca7935c2618d..d8f53bb8616331 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -137,7 +137,7 @@ def test_ExceptionGroup(self): self.assertEqual(str(eg), "flat (2 sub-exceptions)") self.assertEqual(repr(eg), - "ExceptionGroup('flat', (ValueError(1), TypeError(2)))") + "ExceptionGroup('flat', [ValueError(1), TypeError(2)])") # Mutate the list of exceptions passed to BaseExceptionGroup. # This shouldn't change the EG's functionality, nor its repr. @@ -149,11 +149,11 @@ def test_ExceptionGroup(self): self.assertEqual(str(eg), "nested (4 sub-exceptions)") self.assertEqual(repr(eg), "ExceptionGroup('nested', " - "(ExceptionGroup('flat', " - "(ValueError(1), TypeError(2))), " + "[ExceptionGroup('flat', " + "[ValueError(1), TypeError(2)]), " "ValueError(1), " "ExceptionGroup('flat', " - "(ValueError(1), TypeError(2))), TypeError(2)))") + "[ValueError(1), TypeError(2)]), TypeError(2)])") def test_BaseExceptionGroup(self): eg_excs = [ValueError(1), KeyboardInterrupt(2)] @@ -164,7 +164,7 @@ def test_BaseExceptionGroup(self): self.assertEqual(repr(eg), "BaseExceptionGroup(" "'flat', " - "(ValueError(1), KeyboardInterrupt(2)))") + "[ValueError(1), KeyboardInterrupt(2)])") # Mutate the list of exceptions passed to BaseExceptionGroup. # This shouldn't change the EG's functionality, nor its repr. @@ -176,11 +176,11 @@ def test_BaseExceptionGroup(self): self.assertEqual(str(eg), "nested (3 sub-exceptions)") self.assertEqual(repr(eg), "BaseExceptionGroup('nested', " - "(BaseExceptionGroup('flat', " - "(ValueError(1), KeyboardInterrupt(2))), " + "[BaseExceptionGroup('flat', " + "[ValueError(1), KeyboardInterrupt(2)]), " "ValueError(1), " "BaseExceptionGroup('flat', " - "(ValueError(1), KeyboardInterrupt(2)))))") + "[ValueError(1), KeyboardInterrupt(2)])])") def test_custom_exception(self): class MyEG(ExceptionGroup): @@ -191,7 +191,7 @@ class MyEG(ExceptionGroup): 'flat', eg_excs) self.assertEqual(str(eg), "flat (2 sub-exceptions)") - self.assertEqual(repr(eg), "MyEG('flat', (ValueError(1), TypeError(2)))") + self.assertEqual(repr(eg), "MyEG('flat', [ValueError(1), TypeError(2)])") # Mutate the list of exceptions passed to MyEG. # This shouldn't change the EG's functionality, nor its repr. @@ -203,10 +203,10 @@ class MyEG(ExceptionGroup): self.assertEqual(str(eg), "nested (4 sub-exceptions)") self.assertEqual(repr(eg), ( "MyEG('nested', " - "(MyEG('flat', (ValueError(1), TypeError(2))), " + "[MyEG('flat', [ValueError(1), TypeError(2)]), " "ValueError(1), " - "MyEG('flat', (ValueError(1), TypeError(2))), " - "TypeError(2)))")) + "MyEG('flat', [ValueError(1), TypeError(2)]), " + "TypeError(2)])")) def create_simple_eg(): From b45402ab026aafa15c2311376cd7136a2daf6bdd Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sat, 22 Nov 2025 06:34:55 +1030 Subject: [PATCH 09/41] Store exceptions copy in `BaseExceptionGroup->excs_orig` and use in `repr` --- Include/cpython/pyerrors.h | 1 + Objects/exceptions.c | 24 ++++++++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/Include/cpython/pyerrors.h b/Include/cpython/pyerrors.h index 6b63d304b0d929..9e0b58526fd7b3 100644 --- a/Include/cpython/pyerrors.h +++ b/Include/cpython/pyerrors.h @@ -18,6 +18,7 @@ typedef struct { PyException_HEAD PyObject *msg; PyObject *excs; + PyObject *excs_orig; } PyBaseExceptionGroupObject; typedef struct { diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 0dd7e4c4a2125c..c0db185635a891 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -905,12 +905,22 @@ BaseExceptionGroup_new(PyTypeObject *type, PyObject *args, PyObject *kwds) return NULL; } - exceptions = PySequence_Tuple(exceptions); + /* Make a copy of the passed exceptions sequence, for use in the repr */ + PyObject *exceptions_copy = exceptions; + if (PyList_Check(exceptions)) { + exceptions_copy = PySequence_List(exceptions); + } else { + Py_INCREF(exceptions_copy); + } + + exceptions = PySequence_Tuple(exceptions_copy); if (!exceptions) { return NULL; } - /* We are now holding a ref to the exceptions tuple */ + /* We are now holding a ref to (a potential copy of) the original + * exceptions sequence and to the exceptions tuple. + */ Py_ssize_t numexcs = PyTuple_GET_SIZE(exceptions); if (numexcs == 0) { @@ -988,9 +998,11 @@ BaseExceptionGroup_new(PyTypeObject *type, PyObject *args, PyObject *kwds) self->msg = Py_NewRef(message); self->excs = exceptions; + self->excs_orig = exceptions_copy; return (PyObject*)self; error: Py_DECREF(exceptions); + Py_DECREF(exceptions_copy); return NULL; } @@ -1029,6 +1041,7 @@ BaseExceptionGroup_clear(PyObject *op) PyBaseExceptionGroupObject *self = PyBaseExceptionGroupObject_CAST(op); Py_CLEAR(self->msg); Py_CLEAR(self->excs); + Py_CLEAR(self->excs_orig); return BaseException_clear(op); } @@ -1046,6 +1059,7 @@ BaseExceptionGroup_traverse(PyObject *op, visitproc visit, void *arg) PyBaseExceptionGroupObject *self = PyBaseExceptionGroupObject_CAST(op); Py_VISIT(self->msg); Py_VISIT(self->excs); + Py_VISIT(self->excs_orig); return BaseException_traverse(op, visit, arg); } @@ -1068,12 +1082,12 @@ BaseExceptionGroup_repr(PyObject *op) { PyBaseExceptionGroupObject *self = PyBaseExceptionGroupObject_CAST(op); assert(self->msg); - assert(self->excs); + assert(self->excs_orig); const char *name = _PyType_Name(Py_TYPE(self)); return PyUnicode_FromFormat( "%s(%R, %R)", name, - self->msg, self->excs); + self->msg, self->excs_orig); } /*[clinic input] @@ -1695,6 +1709,8 @@ static PyMemberDef BaseExceptionGroup_members[] = { PyDoc_STR("exception message")}, {"exceptions", _Py_T_OBJECT, offsetof(PyBaseExceptionGroupObject, excs), Py_READONLY, PyDoc_STR("nested exceptions")}, + {"_excs_orig", _Py_T_OBJECT, offsetof(PyBaseExceptionGroupObject, excs_orig), Py_READONLY, + PyDoc_STR("private copy of original nested exceptions")}, {NULL} /* Sentinel */ }; From 62d72a40817eae379bcd175c749e6c2e46cc9a99 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sat, 22 Nov 2025 06:43:17 +1030 Subject: [PATCH 10/41] Revert "Update docs for `BaseExceptionGroup` repr change" This reverts commit 493ab01143fe412892781704d0127d120f064092. --- Doc/library/exceptions.rst | 6 +++--- Doc/reference/compound_stmts.rst | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/library/exceptions.rst b/Doc/library/exceptions.rst index 22715c979ab8f2..16d42c010f6df0 100644 --- a/Doc/library/exceptions.rst +++ b/Doc/library/exceptions.rst @@ -1053,11 +1053,11 @@ their subgroups based on the types of the contained exceptions. ... >>> match, rest = exc.split(ValueError) >>> exc, exc.__context__, exc.__cause__, exc.__notes__ - (MyGroup('eg', (ValueError(1), TypeError(2))), Exception('context'), Exception('cause'), ['a note']) + (MyGroup('eg', [ValueError(1), TypeError(2)]), Exception('context'), Exception('cause'), ['a note']) >>> match, match.__context__, match.__cause__, match.__notes__ - (MyGroup('eg', (ValueError(1),)), Exception('context'), Exception('cause'), ['a note']) + (MyGroup('eg', [ValueError(1)]), Exception('context'), Exception('cause'), ['a note']) >>> rest, rest.__context__, rest.__cause__, rest.__notes__ - (MyGroup('eg', (TypeError(2),)), Exception('context'), Exception('cause'), ['a note']) + (MyGroup('eg', [TypeError(2)]), Exception('context'), Exception('cause'), ['a note']) >>> exc.__traceback__ is match.__traceback__ is rest.__traceback__ True diff --git a/Doc/reference/compound_stmts.rst b/Doc/reference/compound_stmts.rst index 861c221502ed4d..6521b4bee50758 100644 --- a/Doc/reference/compound_stmts.rst +++ b/Doc/reference/compound_stmts.rst @@ -388,7 +388,7 @@ type of the target ``e`` is consistently :exc:`BaseExceptionGroup`:: ... except* BlockingIOError as e: ... print(repr(e)) ... - ExceptionGroup('', (BlockingIOError(),)) + ExceptionGroup('', (BlockingIOError())) :keyword:`break`, :keyword:`continue` and :keyword:`return` cannot appear in an :keyword:`!except*` clause. From f93ae8f9e92bb2f7056f6b06114b8b6825b1c76e Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sat, 22 Nov 2025 07:10:51 +1030 Subject: [PATCH 11/41] Remove `BaseExceptionGroup->excs_orig` and reconstruct in `repr` instead --- Include/cpython/pyerrors.h | 1 - Objects/exceptions.c | 29 +++++++++-------------------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/Include/cpython/pyerrors.h b/Include/cpython/pyerrors.h index 9e0b58526fd7b3..6b63d304b0d929 100644 --- a/Include/cpython/pyerrors.h +++ b/Include/cpython/pyerrors.h @@ -18,7 +18,6 @@ typedef struct { PyException_HEAD PyObject *msg; PyObject *excs; - PyObject *excs_orig; } PyBaseExceptionGroupObject; typedef struct { diff --git a/Objects/exceptions.c b/Objects/exceptions.c index c0db185635a891..be3dfcda35e449 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -905,22 +905,12 @@ BaseExceptionGroup_new(PyTypeObject *type, PyObject *args, PyObject *kwds) return NULL; } - /* Make a copy of the passed exceptions sequence, for use in the repr */ - PyObject *exceptions_copy = exceptions; - if (PyList_Check(exceptions)) { - exceptions_copy = PySequence_List(exceptions); - } else { - Py_INCREF(exceptions_copy); - } - - exceptions = PySequence_Tuple(exceptions_copy); + exceptions = PySequence_Tuple(exceptions); if (!exceptions) { return NULL; } - /* We are now holding a ref to (a potential copy of) the original - * exceptions sequence and to the exceptions tuple. - */ + /* We are now holding a ref to the exceptions tuple */ Py_ssize_t numexcs = PyTuple_GET_SIZE(exceptions); if (numexcs == 0) { @@ -998,11 +988,9 @@ BaseExceptionGroup_new(PyTypeObject *type, PyObject *args, PyObject *kwds) self->msg = Py_NewRef(message); self->excs = exceptions; - self->excs_orig = exceptions_copy; return (PyObject*)self; error: Py_DECREF(exceptions); - Py_DECREF(exceptions_copy); return NULL; } @@ -1041,7 +1029,6 @@ BaseExceptionGroup_clear(PyObject *op) PyBaseExceptionGroupObject *self = PyBaseExceptionGroupObject_CAST(op); Py_CLEAR(self->msg); Py_CLEAR(self->excs); - Py_CLEAR(self->excs_orig); return BaseException_clear(op); } @@ -1059,7 +1046,6 @@ BaseExceptionGroup_traverse(PyObject *op, visitproc visit, void *arg) PyBaseExceptionGroupObject *self = PyBaseExceptionGroupObject_CAST(op); Py_VISIT(self->msg); Py_VISIT(self->excs); - Py_VISIT(self->excs_orig); return BaseException_traverse(op, visit, arg); } @@ -1082,12 +1068,17 @@ BaseExceptionGroup_repr(PyObject *op) { PyBaseExceptionGroupObject *self = PyBaseExceptionGroupObject_CAST(op); assert(self->msg); - assert(self->excs_orig); + assert(self->excs); + + PyObject* excs_orig = PyTuple_GET_ITEM(self->args, 1); + if (PyList_Check(excs_orig)) { + excs_orig = PySequence_List(self->excs); + } const char *name = _PyType_Name(Py_TYPE(self)); return PyUnicode_FromFormat( "%s(%R, %R)", name, - self->msg, self->excs_orig); + self->msg, excs_orig); } /*[clinic input] @@ -1709,8 +1700,6 @@ static PyMemberDef BaseExceptionGroup_members[] = { PyDoc_STR("exception message")}, {"exceptions", _Py_T_OBJECT, offsetof(PyBaseExceptionGroupObject, excs), Py_READONLY, PyDoc_STR("nested exceptions")}, - {"_excs_orig", _Py_T_OBJECT, offsetof(PyBaseExceptionGroupObject, excs_orig), Py_READONLY, - PyDoc_STR("private copy of original nested exceptions")}, {NULL} /* Sentinel */ }; From 56eefeef1d10e157b39517ed1fc1ec8c22917d24 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sat, 22 Nov 2025 07:13:50 +1030 Subject: [PATCH 12/41] Ensure that the exceptions copy in `BaseExceptionGroup` repr is freed --- Objects/exceptions.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index be3dfcda35e449..4de5f8494e1961 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -1073,12 +1073,17 @@ BaseExceptionGroup_repr(PyObject *op) PyObject* excs_orig = PyTuple_GET_ITEM(self->args, 1); if (PyList_Check(excs_orig)) { excs_orig = PySequence_List(self->excs); + } else { + Py_INCREF(excs_orig); } const char *name = _PyType_Name(Py_TYPE(self)); - return PyUnicode_FromFormat( + PyObject *repr = PyUnicode_FromFormat( "%s(%R, %R)", name, self->msg, excs_orig); + + Py_DECREF(excs_orig); + return repr; } /*[clinic input] From 79dd61534b8cbdf92e0776ee46d09e2473a2bdc5 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sat, 22 Nov 2025 17:05:30 +1030 Subject: [PATCH 13/41] Move `ExceptionGroup` repr tests into new function --- Lib/test/test_exception_group.py | 43 +++++++++++++++++++------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index d8f53bb8616331..f49f7b27239989 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -131,18 +131,13 @@ class MyBEG(BaseExceptionGroup): class StrAndReprTests(unittest.TestCase): def test_ExceptionGroup(self): - eg_excs = [ValueError(1), TypeError(2)] eg = BaseExceptionGroup( - 'flat', eg_excs) + 'flat', [ValueError(1), TypeError(2)]) self.assertEqual(str(eg), "flat (2 sub-exceptions)") self.assertEqual(repr(eg), "ExceptionGroup('flat', [ValueError(1), TypeError(2)])") - # Mutate the list of exceptions passed to BaseExceptionGroup. - # This shouldn't change the EG's functionality, nor its repr. - eg_excs.clear() - eg = BaseExceptionGroup( 'nested', [eg, ValueError(1), eg, TypeError(2)]) @@ -156,9 +151,8 @@ def test_ExceptionGroup(self): "[ValueError(1), TypeError(2)]), TypeError(2)])") def test_BaseExceptionGroup(self): - eg_excs = [ValueError(1), KeyboardInterrupt(2)] eg = BaseExceptionGroup( - 'flat', eg_excs) + 'flat', [ValueError(1), KeyboardInterrupt(2)]) self.assertEqual(str(eg), "flat (2 sub-exceptions)") self.assertEqual(repr(eg), @@ -166,10 +160,6 @@ def test_BaseExceptionGroup(self): "'flat', " "[ValueError(1), KeyboardInterrupt(2)])") - # Mutate the list of exceptions passed to BaseExceptionGroup. - # This shouldn't change the EG's functionality, nor its repr. - eg_excs.clear() - eg = BaseExceptionGroup( 'nested', [eg, ValueError(1), eg]) @@ -186,17 +176,12 @@ def test_custom_exception(self): class MyEG(ExceptionGroup): pass - eg_excs = [ValueError(1), TypeError(2)] eg = MyEG( - 'flat', eg_excs) + 'flat', [ValueError(1), TypeError(2)]) self.assertEqual(str(eg), "flat (2 sub-exceptions)") self.assertEqual(repr(eg), "MyEG('flat', [ValueError(1), TypeError(2)])") - # Mutate the list of exceptions passed to MyEG. - # This shouldn't change the EG's functionality, nor its repr. - eg_excs.clear() - eg = MyEG( 'nested', [eg, ValueError(1), eg, TypeError(2)]) @@ -208,6 +193,28 @@ class MyEG(ExceptionGroup): "MyEG('flat', [ValueError(1), TypeError(2)]), " "TypeError(2)])")) + def test_exceptions_mutation(self): + class MyEG(ExceptionGroup): + pass + + excs = [ValueError(1), TypeError(2)] + eg = MyEG('test', excs) + + self.assertEqual(repr(eg), "MyEG('test', [ValueError(1), TypeError(2)])") + excs.clear() + + # Ensure that clearing the exceptions sequence doesn't change the repr. + self.assertEqual(repr(eg), "MyEG('test', [ValueError(1), TypeError(2)])") + + # Ensure that the args are still as passed. + self.assertEqual(eg.args, ('test', [])) + + excs = (ValueError(1), KeyboardInterrupt(2)) + eg = BaseExceptionGroup('test', excs) + + # Ensure that other, immutable, sequences still work fine. + self.assertEqual(repr(eg), "BaseExceptionGroup('test', (ValueError(1), KeyboardInterrupt(2)))") + def create_simple_eg(): excs = [] From e9fec3d944e5fa264955b8c54b082f9ea26d51cd Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sun, 23 Nov 2025 07:36:42 +1030 Subject: [PATCH 14/41] Use the exceptions tuple for all non-lists in `ExceptionGroup` repr --- Objects/exceptions.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 4de5f8494e1961..a9a053dc350699 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -1070,11 +1070,13 @@ BaseExceptionGroup_repr(PyObject *op) assert(self->msg); assert(self->excs); + /* Use the actual exceptions tuple for accuracy, but make it look like the + * original exception sequence, if possible, for backwards compatibility. */ PyObject* excs_orig = PyTuple_GET_ITEM(self->args, 1); if (PyList_Check(excs_orig)) { excs_orig = PySequence_List(self->excs); } else { - Py_INCREF(excs_orig); + excs_orig = Py_NewRef(self->excs); } const char *name = _PyType_Name(Py_TYPE(self)); From f5acfefdf5353db5ccbe36fd21812fec6918bcd4 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sun, 23 Nov 2025 07:37:04 +1030 Subject: [PATCH 15/41] Test custom sequence's `ExceptionGroup` repr --- Lib/test/test_exception_group.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index f49f7b27239989..52f1a04d3a996c 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -1,4 +1,4 @@ -import collections.abc +import collections import types import unittest from test.support import skip_emscripten_stack_overflow, skip_wasi_stack_overflow, exceeds_recursion_limit @@ -212,9 +212,19 @@ class MyEG(ExceptionGroup): excs = (ValueError(1), KeyboardInterrupt(2)) eg = BaseExceptionGroup('test', excs) - # Ensure that other, immutable, sequences still work fine. + # Ensure that immutable sequences still work fine. self.assertEqual(repr(eg), "BaseExceptionGroup('test', (ValueError(1), KeyboardInterrupt(2)))") + # Test non-standard custom sequences. + excs = collections.deque([ValueError(1), TypeError(2)]) + eg = ExceptionGroup('test', excs) + + self.assertEqual(repr(eg), "ExceptionGroup('test', (ValueError(1), TypeError(2)))") + excs.clear() + + # Ensure that clearing the exceptions sequence doesn't change the repr. + self.assertEqual(repr(eg), "ExceptionGroup('test', (ValueError(1), TypeError(2)))") + def create_simple_eg(): excs = [] From 2fca592a09b2f6cddb10050d70e83b48a13251ff Mon Sep 17 00:00:00 2001 From: dr-carlos <77367421+dr-carlos@users.noreply.github.com> Date: Sun, 23 Nov 2025 07:46:26 +1030 Subject: [PATCH 16/41] Update code style in `exceptions.c` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Objects/exceptions.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index a9a053dc350699..d40b2aebdc1126 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -1075,7 +1075,8 @@ BaseExceptionGroup_repr(PyObject *op) PyObject* excs_orig = PyTuple_GET_ITEM(self->args, 1); if (PyList_Check(excs_orig)) { excs_orig = PySequence_List(self->excs); - } else { + } + else { excs_orig = Py_NewRef(self->excs); } From 4885a6b17a2ba15a9c4c8e1621397125ed73f3c3 Mon Sep 17 00:00:00 2001 From: dr-carlos <77367421+dr-carlos@users.noreply.github.com> Date: Sun, 23 Nov 2025 07:49:56 +1030 Subject: [PATCH 17/41] Update code style in `exceptions.c` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Objects/exceptions.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index d40b2aebdc1126..273386d6c2fb23 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -1082,8 +1082,7 @@ BaseExceptionGroup_repr(PyObject *op) const char *name = _PyType_Name(Py_TYPE(self)); PyObject *repr = PyUnicode_FromFormat( - "%s(%R, %R)", name, - self->msg, excs_orig); + "%T(%R, %R)", self, self->msg, excs_orig); Py_DECREF(excs_orig); return repr; From 480139929914e5a810067ecb752999495d46bcbc Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Mon, 24 Nov 2025 07:27:26 +1030 Subject: [PATCH 18/41] Remove unused `name` variable with new `%Y` format --- Objects/exceptions.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 273386d6c2fb23..a7264b40c1bfc6 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -1080,7 +1080,6 @@ BaseExceptionGroup_repr(PyObject *op) excs_orig = Py_NewRef(self->excs); } - const char *name = _PyType_Name(Py_TYPE(self)); PyObject *repr = PyUnicode_FromFormat( "%T(%R, %R)", self, self->msg, excs_orig); From 1bcddeb933d2515c9aaef70a80964ebbb0102a79 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Mon, 24 Nov 2025 09:13:37 +1030 Subject: [PATCH 19/41] Revert "Remove unused `name` variable with new `%Y` format" This reverts commit 480139929914e5a810067ecb752999495d46bcbc. --- Objects/exceptions.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index a7264b40c1bfc6..273386d6c2fb23 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -1080,6 +1080,7 @@ BaseExceptionGroup_repr(PyObject *op) excs_orig = Py_NewRef(self->excs); } + const char *name = _PyType_Name(Py_TYPE(self)); PyObject *repr = PyUnicode_FromFormat( "%T(%R, %R)", self, self->msg, excs_orig); From ccef7c55442c0c48e01d9d214804058fe1e3fcce Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Mon, 24 Nov 2025 09:13:52 +1030 Subject: [PATCH 20/41] Revert "Update code style in `exceptions.c`" This reverts commit 4885a6b17a2ba15a9c4c8e1621397125ed73f3c3. --- Objects/exceptions.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 273386d6c2fb23..d40b2aebdc1126 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -1082,7 +1082,8 @@ BaseExceptionGroup_repr(PyObject *op) const char *name = _PyType_Name(Py_TYPE(self)); PyObject *repr = PyUnicode_FromFormat( - "%T(%R, %R)", self, self->msg, excs_orig); + "%s(%R, %R)", name, + self->msg, excs_orig); Py_DECREF(excs_orig); return repr; From 9ca573ecda58c0b25fab399a70402f4b660b246b Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Tue, 25 Nov 2025 13:45:07 +1030 Subject: [PATCH 21/41] Save repr of non-standard sequences in `BaseExceptionGroup` constructor --- Include/cpython/pyerrors.h | 1 + Objects/exceptions.c | 46 ++++++++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/Include/cpython/pyerrors.h b/Include/cpython/pyerrors.h index 6b63d304b0d929..be2e3b641c25cb 100644 --- a/Include/cpython/pyerrors.h +++ b/Include/cpython/pyerrors.h @@ -18,6 +18,7 @@ typedef struct { PyException_HEAD PyObject *msg; PyObject *excs; + PyObject *excs_str; } PyBaseExceptionGroupObject; typedef struct { diff --git a/Objects/exceptions.c b/Objects/exceptions.c index d40b2aebdc1126..5e7c0f1a22cf2c 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -890,6 +890,7 @@ BaseExceptionGroup_new(PyTypeObject *type, PyObject *args, PyObject *kwds) PyObject *message = NULL; PyObject *exceptions = NULL; + PyObject *exceptions_str = NULL; if (!PyArg_ParseTuple(args, "UO:BaseExceptionGroup.__new__", @@ -905,6 +906,11 @@ BaseExceptionGroup_new(PyTypeObject *type, PyObject *args, PyObject *kwds) return NULL; } + /* Save initial exceptions sequence as a string incase sequence is mutated */ + if (!PyList_Check(exceptions) && !PyTuple_Check(exceptions)) { + exceptions_str = PyObject_Repr(exceptions); + } + exceptions = PySequence_Tuple(exceptions); if (!exceptions) { return NULL; @@ -988,9 +994,11 @@ BaseExceptionGroup_new(PyTypeObject *type, PyObject *args, PyObject *kwds) self->msg = Py_NewRef(message); self->excs = exceptions; + self->excs_str = exceptions_str; return (PyObject*)self; error: Py_DECREF(exceptions); + Py_XDECREF(exceptions_str); return NULL; } @@ -1029,6 +1037,7 @@ BaseExceptionGroup_clear(PyObject *op) PyBaseExceptionGroupObject *self = PyBaseExceptionGroupObject_CAST(op); Py_CLEAR(self->msg); Py_CLEAR(self->excs); + Py_CLEAR(self->excs_str); return BaseException_clear(op); } @@ -1046,6 +1055,7 @@ BaseExceptionGroup_traverse(PyObject *op, visitproc visit, void *arg) PyBaseExceptionGroupObject *self = PyBaseExceptionGroupObject_CAST(op); Py_VISIT(self->msg); Py_VISIT(self->excs); + Py_VISIT(self->excs_str); return BaseException_traverse(op, visit, arg); } @@ -1068,24 +1078,34 @@ BaseExceptionGroup_repr(PyObject *op) { PyBaseExceptionGroupObject *self = PyBaseExceptionGroupObject_CAST(op); assert(self->msg); - assert(self->excs); - /* Use the actual exceptions tuple for accuracy, but make it look like the - * original exception sequence, if possible, for backwards compatibility. */ - PyObject* excs_orig = PyTuple_GET_ITEM(self->args, 1); - if (PyList_Check(excs_orig)) { - excs_orig = PySequence_List(self->excs); - } - else { - excs_orig = Py_NewRef(self->excs); + PyObject *exceptions_str = Py_XNewRef(self->excs_str); + + /* If the initial exceptions string was not saved in the constructor. */ + if (!exceptions_str) { + assert(self->excs); + + /* Older versions of this code delegated to BaseException's repr, inserting + * the current value of self.args[1]. However, mutating that sequence makes + * the repr appear as if the ExceptionGroup itself has changed, which it hasn't. + * So we use the actual exceptions tuple for accuracy, but make it look like the + * original exception sequence if possible, for backwards compatibility. */ + if (PyList_Check(PyTuple_GET_ITEM(self->args, 1))) { + PyObject *exceptions_list = PySequence_List(self->excs); + exceptions_str = PyObject_Repr(exceptions_list); + Py_DECREF(exceptions_list); + } + else { + exceptions_str = PyObject_Repr(self->excs); + } } const char *name = _PyType_Name(Py_TYPE(self)); PyObject *repr = PyUnicode_FromFormat( - "%s(%R, %R)", name, - self->msg, excs_orig); + "%s(%R, %U)", name, + self->msg, exceptions_str); - Py_DECREF(excs_orig); + Py_DECREF(exceptions_str); return repr; } @@ -1708,6 +1728,8 @@ static PyMemberDef BaseExceptionGroup_members[] = { PyDoc_STR("exception message")}, {"exceptions", _Py_T_OBJECT, offsetof(PyBaseExceptionGroupObject, excs), Py_READONLY, PyDoc_STR("nested exceptions")}, + {"_exceptions_str", _Py_T_OBJECT, offsetof(PyBaseExceptionGroupObject, excs_str), Py_READONLY, + PyDoc_STR("private string representation of initial exceptions sequence")}, {NULL} /* Sentinel */ }; From 9531fe90f8f5a234b54ed6c4e2f73a15dc448f88 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Tue, 25 Nov 2025 13:46:54 +1030 Subject: [PATCH 22/41] Update tests for non-standard exceptions sequence in `ExceptionGroup` --- Lib/test/test_exception_group.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 52f1a04d3a996c..ecc58e3b7e6e2f 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -219,11 +219,11 @@ class MyEG(ExceptionGroup): excs = collections.deque([ValueError(1), TypeError(2)]) eg = ExceptionGroup('test', excs) - self.assertEqual(repr(eg), "ExceptionGroup('test', (ValueError(1), TypeError(2)))") + self.assertEqual(repr(eg), f"ExceptionGroup('test', deque([ValueError(1), TypeError(2)]))") excs.clear() # Ensure that clearing the exceptions sequence doesn't change the repr. - self.assertEqual(repr(eg), "ExceptionGroup('test', (ValueError(1), TypeError(2)))") + self.assertEqual(repr(eg), "ExceptionGroup('test', deque([ValueError(1), TypeError(2)]))") def create_simple_eg(): From 1da923874152439dc51727e29e9921fbccd92173 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Wed, 26 Nov 2025 08:28:13 +1030 Subject: [PATCH 23/41] Document implementation details for `ExceptionGroup` exception sequence --- Doc/library/exceptions.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Doc/library/exceptions.rst b/Doc/library/exceptions.rst index 16d42c010f6df0..d9b61a0732de8d 100644 --- a/Doc/library/exceptions.rst +++ b/Doc/library/exceptions.rst @@ -978,6 +978,12 @@ their subgroups based on the types of the contained exceptions. raises a :exc:`TypeError` if any contained exception is not an :exc:`Exception` subclass. + .. impl-detail:: + + The ``excs`` parameter may be any sequence, but lists and tuples are + specifically processed more efficiently here. For optimum performance, + pass a tuple as ``excs``. + .. attribute:: message The ``msg`` argument to the constructor. This is a read-only attribute. From fcaf3148e07477535bdf4e64a868651f2c655076 Mon Sep 17 00:00:00 2001 From: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:22:09 +0000 Subject: [PATCH 24/41] Update Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-16-40-24.gh-issue-141732.PTetqp.rst --- .../2025-11-19-16-40-24.gh-issue-141732.PTetqp.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-16-40-24.gh-issue-141732.PTetqp.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-16-40-24.gh-issue-141732.PTetqp.rst index 17848e65bac76c..da96d27598ac43 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-16-40-24.gh-issue-141732.PTetqp.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-16-40-24.gh-issue-141732.PTetqp.rst @@ -1,2 +1,2 @@ Ensure the repr for :exc:`ExceptionGroup` and :exc:`BaseExceptionGroup` does -not change when its original exception list is mutated. +not change when the exception sequence that was original passed in to its constructor is subsequently mutated. From d9625813a0213bf248bde58011071a01efda36cf Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Wed, 26 Nov 2025 22:04:56 +1030 Subject: [PATCH 25/41] Reword 'optimum' to 'optimal' in `exceptions.rst` --- Doc/library/exceptions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/exceptions.rst b/Doc/library/exceptions.rst index d9b61a0732de8d..b5e3a84b4556dd 100644 --- a/Doc/library/exceptions.rst +++ b/Doc/library/exceptions.rst @@ -981,7 +981,7 @@ their subgroups based on the types of the contained exceptions. .. impl-detail:: The ``excs`` parameter may be any sequence, but lists and tuples are - specifically processed more efficiently here. For optimum performance, + specifically processed more efficiently here. For optimal performance, pass a tuple as ``excs``. .. attribute:: message From ce39eec182ff336b85c94380ccb90a6d3cc4f542 Mon Sep 17 00:00:00 2001 From: dr-carlos <77367421+dr-carlos@users.noreply.github.com> Date: Wed, 26 Nov 2025 22:05:43 +1030 Subject: [PATCH 26/41] Update NEWS Entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- .../2025-11-19-16-40-24.gh-issue-141732.PTetqp.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-16-40-24.gh-issue-141732.PTetqp.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-16-40-24.gh-issue-141732.PTetqp.rst index da96d27598ac43..08420fd5f4d18a 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-16-40-24.gh-issue-141732.PTetqp.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-16-40-24.gh-issue-141732.PTetqp.rst @@ -1,2 +1,2 @@ -Ensure the repr for :exc:`ExceptionGroup` and :exc:`BaseExceptionGroup` does +Ensure the :meth:`~object.__repr__` for :exc:`ExceptionGroup` and :exc:`BaseExceptionGroup` does not change when the exception sequence that was original passed in to its constructor is subsequently mutated. From 9211d01a4c401ac79d01e98969318973957d84c6 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Wed, 26 Nov 2025 22:55:34 +1030 Subject: [PATCH 27/41] Handle error on `__repr__` for exceptions sequence passed to `ExceptionGroup()` --- Objects/exceptions.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 5e7c0f1a22cf2c..96732d2a6076c5 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -909,6 +909,13 @@ BaseExceptionGroup_new(PyTypeObject *type, PyObject *args, PyObject *kwds) /* Save initial exceptions sequence as a string incase sequence is mutated */ if (!PyList_Check(exceptions) && !PyTuple_Check(exceptions)) { exceptions_str = PyObject_Repr(exceptions); + if (exceptions_str == NULL) { + /* We don't hold a reference to exceptions, so clear it before + * attempting a decref in the cleanup. + */ + exceptions = NULL; + goto error; + } } exceptions = PySequence_Tuple(exceptions); @@ -997,7 +1004,7 @@ BaseExceptionGroup_new(PyTypeObject *type, PyObject *args, PyObject *kwds) self->excs_str = exceptions_str; return (PyObject*)self; error: - Py_DECREF(exceptions); + Py_XDECREF(exceptions); Py_XDECREF(exceptions_str); return NULL; } From 737206ad4209cedf866f3624c03bf16884f68a8b Mon Sep 17 00:00:00 2001 From: dr-carlos <77367421+dr-carlos@users.noreply.github.com> Date: Wed, 26 Nov 2025 22:57:11 +1030 Subject: [PATCH 28/41] Change grammar in `exceptions.c` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Objects/exceptions.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 96732d2a6076c5..315343afa47415 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -906,7 +906,7 @@ BaseExceptionGroup_new(PyTypeObject *type, PyObject *args, PyObject *kwds) return NULL; } - /* Save initial exceptions sequence as a string incase sequence is mutated */ + /* Save initial exceptions sequence as a string in case sequence is mutated */ if (!PyList_Check(exceptions) && !PyTuple_Check(exceptions)) { exceptions_str = PyObject_Repr(exceptions); if (exceptions_str == NULL) { From 8d17ef32a2b8f53f95d7955bbe609d5006abf19f Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Wed, 26 Nov 2025 22:59:15 +1030 Subject: [PATCH 29/41] Update comment relating to `Py_XNewRef` in `BaseExceptionGroup_repr()` --- Objects/exceptions.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 315343afa47415..e909731c9a6c91 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -1088,7 +1088,8 @@ BaseExceptionGroup_repr(PyObject *op) PyObject *exceptions_str = Py_XNewRef(self->excs_str); - /* If the initial exceptions string was not saved in the constructor. */ + /* If the initial exceptions string was not saved in the constructor, + * our call to Py_XNewRef returns NULL and we now format it on-demand. */ if (!exceptions_str) { assert(self->excs); From 383af2fa929f6fbf2634484114407ec98af8a5ae Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Wed, 26 Nov 2025 23:06:18 +1030 Subject: [PATCH 30/41] Handle `PySequence_List()` and `PyObject_Repr()` errors in `BaseException_repr()` --- Objects/exceptions.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index e909731c9a6c91..4a90eede8e958c 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -1100,6 +1100,10 @@ BaseExceptionGroup_repr(PyObject *op) * original exception sequence if possible, for backwards compatibility. */ if (PyList_Check(PyTuple_GET_ITEM(self->args, 1))) { PyObject *exceptions_list = PySequence_List(self->excs); + if (!exceptions_list) { + goto error; + } + exceptions_str = PyObject_Repr(exceptions_list); Py_DECREF(exceptions_list); } @@ -1108,6 +1112,10 @@ BaseExceptionGroup_repr(PyObject *op) } } + if (!exceptions_str) { + goto error; + } + const char *name = _PyType_Name(Py_TYPE(self)); PyObject *repr = PyUnicode_FromFormat( "%s(%R, %U)", name, @@ -1115,6 +1123,8 @@ BaseExceptionGroup_repr(PyObject *op) Py_DECREF(exceptions_str); return repr; +error: + return NULL; } /*[clinic input] From 53533403d0f8345ac814326e68e4e5f623b07514 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Thu, 27 Nov 2025 07:15:38 +1030 Subject: [PATCH 31/41] Conditionally use `Py_NewRef` in `BaseExceptionGroup_repr()` --- Objects/exceptions.c | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 4a90eede8e958c..e9999b56bf4a45 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -1086,11 +1086,10 @@ BaseExceptionGroup_repr(PyObject *op) PyBaseExceptionGroupObject *self = PyBaseExceptionGroupObject_CAST(op); assert(self->msg); - PyObject *exceptions_str = Py_XNewRef(self->excs_str); + PyObject *exceptions_str = NULL; - /* If the initial exceptions string was not saved in the constructor, - * our call to Py_XNewRef returns NULL and we now format it on-demand. */ - if (!exceptions_str) { + /* If the initial exceptions string was not saved in the constructor. */ + if (!self->excs_str) { assert(self->excs); /* Older versions of this code delegated to BaseException's repr, inserting @@ -1110,12 +1109,16 @@ BaseExceptionGroup_repr(PyObject *op) else { exceptions_str = PyObject_Repr(self->excs); } - } - if (!exceptions_str) { - goto error; + if (!exceptions_str) { + goto error; + } + } else { + exceptions_str = Py_NewRef(self->excs_str); } + assert(exceptions_str != NULL); + const char *name = _PyType_Name(Py_TYPE(self)); PyObject *repr = PyUnicode_FromFormat( "%s(%R, %U)", name, From 9997c012f74103067a03c2e0bf275c9e009bd06e Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Thu, 27 Nov 2025 07:16:59 +1030 Subject: [PATCH 32/41] Refactor `goto error` into `return NULL` within `BaseExceptionGroup_repr()` --- Objects/exceptions.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index e9999b56bf4a45..1b7f3574f99139 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -1100,7 +1100,7 @@ BaseExceptionGroup_repr(PyObject *op) if (PyList_Check(PyTuple_GET_ITEM(self->args, 1))) { PyObject *exceptions_list = PySequence_List(self->excs); if (!exceptions_list) { - goto error; + return NULL; } exceptions_str = PyObject_Repr(exceptions_list); @@ -1111,7 +1111,7 @@ BaseExceptionGroup_repr(PyObject *op) } if (!exceptions_str) { - goto error; + return NULL; } } else { exceptions_str = Py_NewRef(self->excs_str); @@ -1126,8 +1126,6 @@ BaseExceptionGroup_repr(PyObject *op) Py_DECREF(exceptions_str); return repr; -error: - return NULL; } /*[clinic input] From 3cd38db973ebf9cdb20d24af86db08c3b090f865 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Thu, 27 Nov 2025 08:50:24 +1030 Subject: [PATCH 33/41] Test custom sequences which raise errors in the `ExceptionGroup()` constructor --- Lib/test/test_exception_group.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index ecc58e3b7e6e2f..ae84fceca7a1eb 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -225,6 +225,31 @@ class MyEG(ExceptionGroup): # Ensure that clearing the exceptions sequence doesn't change the repr. self.assertEqual(repr(eg), "ExceptionGroup('test', deque([ValueError(1), TypeError(2)]))") + def test_repr_raises(self): + class MySeq(collections.abc.Sequence): + def __init__(self, raises): + self.raises = raises + + def __len__(self): + return 1 + + def __getitem__(self, index): + if index == 0: + return ValueError(1) + raise IndexError + + def __repr__(self): + if self.raises: + raise self.raises + return None + + with self.assertRaisesRegex(TypeError, r".*MySeq\.__repr__\(\) must return a str, not NoneType"): + ExceptionGroup("test", MySeq(None)) + + with self.assertRaises(ValueError): + BaseExceptionGroup("test", MySeq(ValueError)) + + def create_simple_eg(): excs = [] From 5feb9700b773495c493dbb2b495dd6799fc630a5 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Thu, 27 Nov 2025 08:51:47 +1030 Subject: [PATCH 34/41] Fix formatting of if-else in `exceptions.c` --- Objects/exceptions.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 1b7f3574f99139..daa5505e427d30 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2914,7 +2914,8 @@ SyntaxError_str(PyObject *op) filename = my_basename(self->filename); if (filename == NULL) return NULL; - } else { + } + else { filename = NULL; } have_lineno = (self->lineno != NULL) && PyLong_CheckExact(self->lineno); From d7aec516e4c2175fb444af507aecdf4f84ef0261 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Thu, 27 Nov 2025 08:52:23 +1030 Subject: [PATCH 35/41] Simplify f-string to normal str in `test_exceptions_mutation()` --- Lib/test/test_exception_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index ae84fceca7a1eb..6f8a84350bc6ce 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -219,7 +219,7 @@ class MyEG(ExceptionGroup): excs = collections.deque([ValueError(1), TypeError(2)]) eg = ExceptionGroup('test', excs) - self.assertEqual(repr(eg), f"ExceptionGroup('test', deque([ValueError(1), TypeError(2)]))") + self.assertEqual(repr(eg), "ExceptionGroup('test', deque([ValueError(1), TypeError(2)]))") excs.clear() # Ensure that clearing the exceptions sequence doesn't change the repr. From e21fad064bc263866a5e76f7f19e8da0c3d5d966 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Thu, 27 Nov 2025 08:56:27 +1030 Subject: [PATCH 36/41] Remove unnecessary `_exceptions_str` member of `ExceptionGroup` --- Objects/exceptions.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index daa5505e427d30..22b71c60bf10b0 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -1747,8 +1747,6 @@ static PyMemberDef BaseExceptionGroup_members[] = { PyDoc_STR("exception message")}, {"exceptions", _Py_T_OBJECT, offsetof(PyBaseExceptionGroupObject, excs), Py_READONLY, PyDoc_STR("nested exceptions")}, - {"_exceptions_str", _Py_T_OBJECT, offsetof(PyBaseExceptionGroupObject, excs_str), Py_READONLY, - PyDoc_STR("private string representation of initial exceptions sequence")}, {NULL} /* Sentinel */ }; From 79094f6e85e7f52d6975b2d36ef01f4228075ac0 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Thu, 27 Nov 2025 08:58:05 +1030 Subject: [PATCH 37/41] Fix if-else formatting in `exceptions.c` --- Objects/exceptions.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 22b71c60bf10b0..3435a11814a0a6 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -1113,7 +1113,8 @@ BaseExceptionGroup_repr(PyObject *op) if (!exceptions_str) { return NULL; } - } else { + } + else { exceptions_str = Py_NewRef(self->excs_str); } From b5fcfa78c9890c9a7b7068b168341f1902ca2a32 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Thu, 27 Nov 2025 09:22:27 +1030 Subject: [PATCH 38/41] Keep lines less than 80 chars --- Lib/test/test_exception_group.py | 20 ++++++++++++++++---- Objects/exceptions.c | 9 ++++----- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 6f8a84350bc6ce..488889cde41437 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -213,17 +213,26 @@ class MyEG(ExceptionGroup): eg = BaseExceptionGroup('test', excs) # Ensure that immutable sequences still work fine. - self.assertEqual(repr(eg), "BaseExceptionGroup('test', (ValueError(1), KeyboardInterrupt(2)))") + self.assertEqual( + repr(eg), + "BaseExceptionGroup('test', (ValueError(1), KeyboardInterrupt(2)))" + ) # Test non-standard custom sequences. excs = collections.deque([ValueError(1), TypeError(2)]) eg = ExceptionGroup('test', excs) - self.assertEqual(repr(eg), "ExceptionGroup('test', deque([ValueError(1), TypeError(2)]))") + self.assertEqual( + repr(eg), + "ExceptionGroup('test', deque([ValueError(1), TypeError(2)]))" + ) excs.clear() # Ensure that clearing the exceptions sequence doesn't change the repr. - self.assertEqual(repr(eg), "ExceptionGroup('test', deque([ValueError(1), TypeError(2)]))") + self.assertEqual( + repr(eg), + "ExceptionGroup('test', deque([ValueError(1), TypeError(2)]))" + ) def test_repr_raises(self): class MySeq(collections.abc.Sequence): @@ -243,7 +252,10 @@ def __repr__(self): raise self.raises return None - with self.assertRaisesRegex(TypeError, r".*MySeq\.__repr__\(\) must return a str, not NoneType"): + with self.assertRaisesRegex( + TypeError, + r".*MySeq\.__repr__\(\) must return a str, not NoneType" + ): ExceptionGroup("test", MySeq(None)) with self.assertRaises(ValueError): diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 3435a11814a0a6..507b1166b87bec 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -1092,11 +1092,10 @@ BaseExceptionGroup_repr(PyObject *op) if (!self->excs_str) { assert(self->excs); - /* Older versions of this code delegated to BaseException's repr, inserting - * the current value of self.args[1]. However, mutating that sequence makes - * the repr appear as if the ExceptionGroup itself has changed, which it hasn't. - * So we use the actual exceptions tuple for accuracy, but make it look like the - * original exception sequence if possible, for backwards compatibility. */ + /* Older versions delegated to BaseException, inserting the current + * value of self.args[1]; but this can be mutable and go out-of-sync + * with self.exceptions. Instead, use self.exceptions for accuracy, + * making it look like self.args[1] for backwards compatibility. */ if (PyList_Check(PyTuple_GET_ITEM(self->args, 1))) { PyObject *exceptions_list = PySequence_List(self->excs); if (!exceptions_list) { From 57f8bf62984cb86953c05ee7751c0853fe2a838e Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Fri, 28 Nov 2025 08:15:00 +1030 Subject: [PATCH 39/41] Revert "Fix formatting of if-else in `exceptions.c`" This reverts commit 5feb9700b773495c493dbb2b495dd6799fc630a5. --- Objects/exceptions.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 507b1166b87bec..f0af2bbad9dcb1 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2912,8 +2912,7 @@ SyntaxError_str(PyObject *op) filename = my_basename(self->filename); if (filename == NULL) return NULL; - } - else { + } else { filename = NULL; } have_lineno = (self->lineno != NULL) && PyLong_CheckExact(self->lineno); From 9cba6b64ab0815b727ee1a08ae6ea390117bf5b3 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Fri, 28 Nov 2025 08:16:39 +1030 Subject: [PATCH 40/41] Construct custom sequence outside of `assertRaises` context manager in `test_repr_raises()` --- Lib/test/test_exception_group.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 488889cde41437..ace7ec72917934 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -252,14 +252,16 @@ def __repr__(self): raise self.raises return None + seq = MySeq(None) with self.assertRaisesRegex( TypeError, r".*MySeq\.__repr__\(\) must return a str, not NoneType" ): - ExceptionGroup("test", MySeq(None)) + ExceptionGroup("test", seq) + seq = MySeq(ValueError) with self.assertRaises(ValueError): - BaseExceptionGroup("test", MySeq(ValueError)) + BaseExceptionGroup("test", seq) From d61e6b9b3e006834b92a58e27a0ef3767d014704 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Fri, 28 Nov 2025 08:19:04 +1030 Subject: [PATCH 41/41] Swap order of if-else branches and clarify comment in `BaseExceptionGroup_repr()` --- Objects/exceptions.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index f0af2bbad9dcb1..9a43057b383d29 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -1088,8 +1088,11 @@ BaseExceptionGroup_repr(PyObject *op) PyObject *exceptions_str = NULL; - /* If the initial exceptions string was not saved in the constructor. */ - if (!self->excs_str) { + /* Use the saved exceptions string for custom sequences. */ + if (self->excs_str) { + exceptions_str = Py_NewRef(self->excs_str); + } + else { assert(self->excs); /* Older versions delegated to BaseException, inserting the current @@ -1113,9 +1116,6 @@ BaseExceptionGroup_repr(PyObject *op) return NULL; } } - else { - exceptions_str = Py_NewRef(self->excs_str); - } assert(exceptions_str != NULL);