[Python-checkins] bpo-45292: [PEP 654] add the ExceptionGroup and BaseExceptionGroup classes (GH-28569)

iritkatriel webhook-mailer at python.org
Fri Oct 22 19:13:51 EDT 2021


https://github.com/python/cpython/commit/f30ad65dbf3c6b1b5eec14dc954d65ef32327857
commit: f30ad65dbf3c6b1b5eec14dc954d65ef32327857
branch: main
author: Irit Katriel <1055913+iritkatriel at users.noreply.github.com>
committer: iritkatriel <1055913+iritkatriel at users.noreply.github.com>
date: 2021-10-23T00:13:46+01:00
summary:

bpo-45292: [PEP 654] add the ExceptionGroup and BaseExceptionGroup classes (GH-28569)

files:
A Lib/test/test_exception_group.py
A Misc/NEWS.d/next/Core and Builtins/2021-09-26-18-18-50.bpo-45292.aX5HVr.rst
M Doc/data/stable_abi.dat
M Include/cpython/pyerrors.h
M Include/internal/pycore_interp.h
M Include/internal/pycore_pylifecycle.h
M Include/pyerrors.h
M Lib/test/exception_hierarchy.txt
M Lib/test/test_descr.py
M Lib/test/test_doctest.py
M Lib/test/test_pickle.py
M Lib/test/test_stable_abi_ctypes.py
M Misc/stable_abi.txt
M Objects/exceptions.c
M PC/python3dll.c
M Python/pylifecycle.c

diff --git a/Doc/data/stable_abi.dat b/Doc/data/stable_abi.dat
index 46ee321b660c3..64a0a2a247cd2 100644
--- a/Doc/data/stable_abi.dat
+++ b/Doc/data/stable_abi.dat
@@ -189,6 +189,7 @@ var,PyExc_ArithmeticError,3.2,
 var,PyExc_AssertionError,3.2,
 var,PyExc_AttributeError,3.2,
 var,PyExc_BaseException,3.2,
+var,PyExc_BaseExceptionGroup,3.11,
 var,PyExc_BlockingIOError,3.7,
 var,PyExc_BrokenPipeError,3.7,
 var,PyExc_BufferError,3.2,
diff --git a/Include/cpython/pyerrors.h b/Include/cpython/pyerrors.h
index a3ec5afdb7c78..28ab565dde423 100644
--- a/Include/cpython/pyerrors.h
+++ b/Include/cpython/pyerrors.h
@@ -14,6 +14,12 @@ typedef struct {
     PyException_HEAD
 } PyBaseExceptionObject;
 
+typedef struct {
+    PyException_HEAD
+    PyObject *msg;
+    PyObject *excs;
+} PyBaseExceptionGroupObject;
+
 typedef struct {
     PyException_HEAD
     PyObject *msg;
diff --git a/Include/internal/pycore_interp.h b/Include/internal/pycore_interp.h
index 64ac3abe00fa0..c16f0a4b5e643 100644
--- a/Include/internal/pycore_interp.h
+++ b/Include/internal/pycore_interp.h
@@ -205,6 +205,8 @@ struct _Py_exc_state {
     PyObject *errnomap;
     PyBaseExceptionObject *memerrors_freelist;
     int memerrors_numfree;
+    // The ExceptionGroup type
+    PyObject *PyExc_ExceptionGroup;
 };
 
 
diff --git a/Include/internal/pycore_pylifecycle.h b/Include/internal/pycore_pylifecycle.h
index 4f12fef8d6546..53b94748b32e9 100644
--- a/Include/internal/pycore_pylifecycle.h
+++ b/Include/internal/pycore_pylifecycle.h
@@ -93,6 +93,7 @@ extern void _PyAsyncGen_Fini(PyInterpreterState *interp);
 extern int _PySignal_Init(int install_signal_handlers);
 extern void _PySignal_Fini(void);
 
+extern void _PyExc_ClearExceptionGroupType(PyInterpreterState *interp);
 extern void _PyExc_Fini(PyInterpreterState *interp);
 extern void _PyImport_Fini(void);
 extern void _PyImport_Fini2(void);
diff --git a/Include/pyerrors.h b/Include/pyerrors.h
index c6c443a2d7d0f..77d791427d492 100644
--- a/Include/pyerrors.h
+++ b/Include/pyerrors.h
@@ -60,11 +60,14 @@ PyAPI_FUNC(const char *) PyExceptionClass_Name(PyObject *);
 
 #define PyExceptionInstance_Class(x) ((PyObject*)Py_TYPE(x))
 
+#define _PyBaseExceptionGroup_Check(x)                   \
+    PyObject_TypeCheck(x, (PyTypeObject *)PyExc_BaseExceptionGroup)
 
 /* Predefined exceptions */
 
 PyAPI_DATA(PyObject *) PyExc_BaseException;
 PyAPI_DATA(PyObject *) PyExc_Exception;
+PyAPI_DATA(PyObject *) PyExc_BaseExceptionGroup;
 #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x03050000
 PyAPI_DATA(PyObject *) PyExc_StopAsyncIteration;
 #endif
diff --git a/Lib/test/exception_hierarchy.txt b/Lib/test/exception_hierarchy.txt
index cf54454e71afa..5c0bfda373794 100644
--- a/Lib/test/exception_hierarchy.txt
+++ b/Lib/test/exception_hierarchy.txt
@@ -2,7 +2,9 @@ BaseException
  ├── SystemExit
  ├── KeyboardInterrupt
  ├── GeneratorExit
+ ├── BaseExceptionGroup
  └── Exception
+      ├── ExceptionGroup [BaseExceptionGroup]
       ├── StopIteration
       ├── StopAsyncIteration
       ├── ArithmeticError
diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py
index a5404b30d2459..a4131bec602ea 100644
--- a/Lib/test/test_descr.py
+++ b/Lib/test/test_descr.py
@@ -4032,7 +4032,11 @@ def test_builtin_bases(self):
         for tp in builtin_types:
             object.__getattribute__(tp, "__bases__")
             if tp is not object:
-                self.assertEqual(len(tp.__bases__), 1, tp)
+                if tp is ExceptionGroup:
+                    num_bases = 2
+                else:
+                    num_bases = 1
+                self.assertEqual(len(tp.__bases__), num_bases, tp)
 
         class L(list):
             pass
diff --git a/Lib/test/test_doctest.py b/Lib/test/test_doctest.py
index 3524a0a797c41..8423cafa8c796 100644
--- a/Lib/test/test_doctest.py
+++ b/Lib/test/test_doctest.py
@@ -668,7 +668,7 @@ def non_Python_modules(): r"""
 
     >>> import builtins
     >>> tests = doctest.DocTestFinder().find(builtins)
-    >>> 820 < len(tests) < 840 # approximate number of objects with docstrings
+    >>> 825 < len(tests) < 845 # approximate number of objects with docstrings
     True
     >>> real_tests = [t for t in tests if len(t.examples) > 0]
     >>> len(real_tests) # objects that actually have doctests
diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py
new file mode 100644
index 0000000000000..5bb6094cde742
--- /dev/null
+++ b/Lib/test/test_exception_group.py
@@ -0,0 +1,808 @@
+import collections.abc
+import traceback
+import types
+import unittest
+
+
+class TestExceptionGroupTypeHierarchy(unittest.TestCase):
+    def test_exception_group_types(self):
+        self.assertTrue(issubclass(ExceptionGroup, Exception))
+        self.assertTrue(issubclass(ExceptionGroup, BaseExceptionGroup))
+        self.assertTrue(issubclass(BaseExceptionGroup, BaseException))
+
+    def test_exception_is_not_generic_type(self):
+        with self.assertRaises(TypeError):
+            Exception[OSError]
+
+    def test_exception_group_is_generic_type(self):
+        E = OSError
+        self.assertIsInstance(ExceptionGroup[E], types.GenericAlias)
+        self.assertIsInstance(BaseExceptionGroup[E], types.GenericAlias)
+
+
+class BadConstructorArgs(unittest.TestCase):
+    def test_bad_EG_construction__too_many_args(self):
+        MSG = 'function takes exactly 2 arguments'
+        with self.assertRaisesRegex(TypeError, MSG):
+            ExceptionGroup('no errors')
+        with self.assertRaisesRegex(TypeError, MSG):
+            ExceptionGroup([ValueError('no msg')])
+        with self.assertRaisesRegex(TypeError, MSG):
+            ExceptionGroup('eg', [ValueError('too')], [TypeError('many')])
+
+    def test_bad_EG_construction__bad_message(self):
+        MSG = 'argument 1 must be str, not '
+        with self.assertRaisesRegex(TypeError, MSG):
+            ExceptionGroup(ValueError(12), SyntaxError('bad syntax'))
+        with self.assertRaisesRegex(TypeError, MSG):
+            ExceptionGroup(None, [ValueError(12)])
+
+    def test_bad_EG_construction__bad_excs_sequence(self):
+        MSG = 'second argument \(exceptions\) must be a sequence'
+        with self.assertRaisesRegex(TypeError, MSG):
+            ExceptionGroup('errors not sequence', {ValueError(42)})
+        with self.assertRaisesRegex(TypeError, MSG):
+            ExceptionGroup("eg", None)
+
+        MSG = 'second argument \(exceptions\) must be a non-empty sequence'
+        with self.assertRaisesRegex(ValueError, MSG):
+            ExceptionGroup("eg", [])
+
+    def test_bad_EG_construction__nested_non_exceptions(self):
+        MSG = ('Item [0-9]+ of second argument \(exceptions\)'
+              ' is not an exception')
+        with self.assertRaisesRegex(ValueError, MSG):
+            ExceptionGroup('expect instance, not type', [OSError]);
+        with self.assertRaisesRegex(ValueError, MSG):
+            ExceptionGroup('bad error', ["not an exception"])
+
+
+class InstanceCreation(unittest.TestCase):
+    def test_EG_wraps_Exceptions__creates_EG(self):
+        excs = [ValueError(1), TypeError(2)]
+        self.assertIs(
+            type(ExceptionGroup("eg", excs)),
+            ExceptionGroup)
+
+    def test_BEG_wraps_Exceptions__creates_EG(self):
+        excs = [ValueError(1), TypeError(2)]
+        self.assertIs(
+            type(BaseExceptionGroup("beg", excs)),
+            ExceptionGroup)
+
+    def test_EG_wraps_BaseException__raises_TypeError(self):
+        MSG= "Cannot nest BaseExceptions in an ExceptionGroup"
+        with self.assertRaisesRegex(TypeError, MSG):
+            eg = ExceptionGroup("eg", [ValueError(1), KeyboardInterrupt(2)])
+
+    def test_BEG_wraps_BaseException__creates_BEG(self):
+        beg = BaseExceptionGroup("beg", [ValueError(1), KeyboardInterrupt(2)])
+        self.assertIs(type(beg), BaseExceptionGroup)
+
+    def test_EG_subclass_wraps_anything(self):
+        class MyEG(ExceptionGroup):
+            pass
+
+        self.assertIs(
+            type(MyEG("eg", [ValueError(12), TypeError(42)])),
+            MyEG)
+        self.assertIs(
+            type(MyEG("eg", [ValueError(12), KeyboardInterrupt(42)])),
+            MyEG)
+
+    def test_BEG_subclass_wraps_anything(self):
+        class MyBEG(BaseExceptionGroup):
+            pass
+
+        self.assertIs(
+            type(MyBEG("eg", [ValueError(12), TypeError(42)])),
+            MyBEG)
+        self.assertIs(
+            type(MyBEG("eg", [ValueError(12), KeyboardInterrupt(42)])),
+            MyBEG)
+
+
+def create_simple_eg():
+    excs = []
+    try:
+        try:
+            raise MemoryError("context and cause for ValueError(1)")
+        except MemoryError as e:
+            raise ValueError(1) from e
+    except ValueError as e:
+        excs.append(e)
+
+    try:
+        try:
+            raise OSError("context for TypeError")
+        except OSError as e:
+            raise TypeError(int)
+    except TypeError as e:
+        excs.append(e)
+
+    try:
+        try:
+            raise ImportError("context for ValueError(2)")
+        except ImportError as e:
+            raise ValueError(2)
+    except ValueError as e:
+        excs.append(e)
+
+    try:
+        raise ExceptionGroup('simple eg', excs)
+    except ExceptionGroup as e:
+        return e
+
+
+class ExceptionGroupFields(unittest.TestCase):
+    def test_basics_ExceptionGroup_fields(self):
+        eg = create_simple_eg()
+
+        # check msg
+        self.assertEqual(eg.message, 'simple eg')
+        self.assertEqual(eg.args[0], 'simple eg')
+
+        # check cause and context
+        self.assertIsInstance(eg.exceptions[0], ValueError)
+        self.assertIsInstance(eg.exceptions[0].__cause__, MemoryError)
+        self.assertIsInstance(eg.exceptions[0].__context__, MemoryError)
+        self.assertIsInstance(eg.exceptions[1], TypeError)
+        self.assertIsNone(eg.exceptions[1].__cause__)
+        self.assertIsInstance(eg.exceptions[1].__context__, OSError)
+        self.assertIsInstance(eg.exceptions[2], ValueError)
+        self.assertIsNone(eg.exceptions[2].__cause__)
+        self.assertIsInstance(eg.exceptions[2].__context__, ImportError)
+
+        # check tracebacks
+        line0 = create_simple_eg.__code__.co_firstlineno
+        tb_linenos = [line0 + 27,
+                      [line0 + 6, line0 + 14, line0 + 22]]
+        self.assertEqual(eg.__traceback__.tb_lineno, tb_linenos[0])
+        self.assertIsNone(eg.__traceback__.tb_next)
+        for i in range(3):
+            tb = eg.exceptions[i].__traceback__
+            self.assertIsNone(tb.tb_next)
+            self.assertEqual(tb.tb_lineno, tb_linenos[1][i])
+
+    def test_fields_are_readonly(self):
+        eg = ExceptionGroup('eg', [TypeError(1), OSError(2)])
+
+        self.assertEqual(type(eg.exceptions), tuple)
+
+        eg.message
+        with self.assertRaises(AttributeError):
+            eg.message = "new msg"
+
+        eg.exceptions
+        with self.assertRaises(AttributeError):
+            eg.exceptions = [OSError('xyz')]
+
+
+class ExceptionGroupTestBase(unittest.TestCase):
+    def assertMatchesTemplate(self, exc, exc_type, template):
+        """ Assert that the exception matches the template
+
+            A template describes the shape of exc. If exc is a
+            leaf exception (i.e., not an exception group) then
+            template is an exception instance that has the
+            expected type and args value of exc. If exc is an
+            exception group, then template is a list of the
+            templates of its nested exceptions.
+        """
+        if exc_type is not None:
+            self.assertIs(type(exc), exc_type)
+
+        if isinstance(exc, BaseExceptionGroup):
+            self.assertIsInstance(template, collections.abc.Sequence)
+            self.assertEqual(len(exc.exceptions), len(template))
+            for e, t in zip(exc.exceptions, template):
+                self.assertMatchesTemplate(e, None, t)
+        else:
+            self.assertIsInstance(template, BaseException)
+            self.assertEqual(type(exc), type(template))
+            self.assertEqual(exc.args, template.args)
+
+
+class ExceptionGroupSubgroupTests(ExceptionGroupTestBase):
+    def setUp(self):
+        self.eg = create_simple_eg()
+        self.eg_template = [ValueError(1), TypeError(int), ValueError(2)]
+
+    def test_basics_subgroup_split__bad_arg_type(self):
+        bad_args = ["bad arg",
+                    OSError('instance not type'),
+                    [OSError('instance not type')],]
+        for arg in bad_args:
+            with self.assertRaises(TypeError):
+                self.eg.subgroup(arg)
+            with self.assertRaises(TypeError):
+                self.eg.split(arg)
+
+    def test_basics_subgroup_by_type__passthrough(self):
+        eg = self.eg
+        self.assertIs(eg, eg.subgroup(BaseException))
+        self.assertIs(eg, eg.subgroup(Exception))
+        self.assertIs(eg, eg.subgroup(BaseExceptionGroup))
+        self.assertIs(eg, eg.subgroup(ExceptionGroup))
+
+    def test_basics_subgroup_by_type__no_match(self):
+        self.assertIsNone(self.eg.subgroup(OSError))
+
+    def test_basics_subgroup_by_type__match(self):
+        eg = self.eg
+        testcases = [
+            # (match_type, result_template)
+            (ValueError, [ValueError(1), ValueError(2)]),
+            (TypeError, [TypeError(int)]),
+            ((ValueError, TypeError), self.eg_template)]
+
+        for match_type, template in testcases:
+            with self.subTest(match=match_type):
+                subeg = eg.subgroup(match_type)
+                self.assertEqual(subeg.message, eg.message)
+                self.assertMatchesTemplate(subeg, ExceptionGroup, template)
+
+    def test_basics_subgroup_by_predicate__passthrough(self):
+        self.assertIs(self.eg, self.eg.subgroup(lambda e: True))
+
+    def test_basics_subgroup_by_predicate__no_match(self):
+        self.assertIsNone(self.eg.subgroup(lambda e: False))
+
+    def test_basics_subgroup_by_predicate__match(self):
+        eg = self.eg
+        testcases = [
+            # (match_type, result_template)
+            (ValueError, [ValueError(1), ValueError(2)]),
+            (TypeError, [TypeError(int)]),
+            ((ValueError, TypeError), self.eg_template)]
+
+        for match_type, template in testcases:
+            subeg = eg.subgroup(lambda e: isinstance(e, match_type))
+            self.assertEqual(subeg.message, eg.message)
+            self.assertMatchesTemplate(subeg, ExceptionGroup, template)
+
+
+class ExceptionGroupSplitTests(ExceptionGroupTestBase):
+    def setUp(self):
+        self.eg = create_simple_eg()
+        self.eg_template = [ValueError(1), TypeError(int), ValueError(2)]
+
+    def test_basics_split_by_type__passthrough(self):
+        for E in [BaseException, Exception,
+                  BaseExceptionGroup, ExceptionGroup]:
+            match, rest = self.eg.split(E)
+            self.assertMatchesTemplate(
+                match, ExceptionGroup, self.eg_template)
+            self.assertIsNone(rest)
+
+    def test_basics_split_by_type__no_match(self):
+        match, rest = self.eg.split(OSError)
+        self.assertIsNone(match)
+        self.assertMatchesTemplate(
+            rest, ExceptionGroup, self.eg_template)
+
+    def test_basics_split_by_type__match(self):
+        eg = self.eg
+        VE = ValueError
+        TE = TypeError
+        testcases = [
+            # (matcher, match_template, rest_template)
+            (VE, [VE(1), VE(2)], [TE(int)]),
+            (TE, [TE(int)], [VE(1), VE(2)]),
+            ((VE, TE), self.eg_template, None),
+            ((OSError, VE), [VE(1), VE(2)], [TE(int)]),
+        ]
+
+        for match_type, match_template, rest_template in testcases:
+            match, rest = eg.split(match_type)
+            self.assertEqual(match.message, eg.message)
+            self.assertMatchesTemplate(
+                match, ExceptionGroup, match_template)
+            if rest_template is not None:
+                self.assertEqual(rest.message, eg.message)
+                self.assertMatchesTemplate(
+                    rest, ExceptionGroup, rest_template)
+            else:
+                self.assertIsNone(rest)
+
+    def test_basics_split_by_predicate__passthrough(self):
+        match, rest = self.eg.split(lambda e: True)
+        self.assertMatchesTemplate(match, ExceptionGroup, self.eg_template)
+        self.assertIsNone(rest)
+
+    def test_basics_split_by_predicate__no_match(self):
+        match, rest = self.eg.split(lambda e: False)
+        self.assertIsNone(match)
+        self.assertMatchesTemplate(rest, ExceptionGroup, self.eg_template)
+
+    def test_basics_split_by_predicate__match(self):
+        eg = self.eg
+        VE = ValueError
+        TE = TypeError
+        testcases = [
+            # (matcher, match_template, rest_template)
+            (VE, [VE(1), VE(2)], [TE(int)]),
+            (TE, [TE(int)], [VE(1), VE(2)]),
+            ((VE, TE), self.eg_template, None),
+        ]
+
+        for match_type, match_template, rest_template in testcases:
+            match, rest = eg.split(lambda e: isinstance(e, match_type))
+            self.assertEqual(match.message, eg.message)
+            self.assertMatchesTemplate(
+                match, ExceptionGroup, match_template)
+            if rest_template is not None:
+                self.assertEqual(rest.message, eg.message)
+                self.assertMatchesTemplate(
+                    rest, ExceptionGroup, rest_template)
+
+
+class DeepRecursionInSplitAndSubgroup(unittest.TestCase):
+    def make_deep_eg(self):
+        e = TypeError(1)
+        for i in range(2000):
+            e = ExceptionGroup('eg', [e])
+        return e
+
+    def test_deep_split(self):
+        e = self.make_deep_eg()
+        with self.assertRaises(RecursionError):
+            e.split(TypeError)
+
+    def test_deep_subgroup(self):
+        e = self.make_deep_eg()
+        with self.assertRaises(RecursionError):
+            e.subgroup(TypeError)
+
+
+def leaf_generator(exc, tbs=None):
+    if tbs is None:
+        tbs = []
+    tbs.append(exc.__traceback__)
+    if isinstance(exc, BaseExceptionGroup):
+        for e in exc.exceptions:
+            yield from leaf_generator(e, tbs)
+    else:
+        # exc is a leaf exception and its traceback
+        # is the concatenation of the traceback
+        # segments in tbs
+        yield exc, tbs
+    tbs.pop()
+
+
+class LeafGeneratorTest(unittest.TestCase):
+    # The leaf_generator is mentioned in PEP 654 as a suggestion
+    # on how to iterate over leaf nodes of an EG. Is is also
+    # used below as a test utility. So we test it here.
+
+    def test_leaf_generator(self):
+        eg = create_simple_eg()
+
+        self.assertSequenceEqual(
+            [e for e, _ in leaf_generator(eg)],
+            eg.exceptions)
+
+        for e, tbs in leaf_generator(eg):
+            self.assertSequenceEqual(
+                tbs, [eg.__traceback__, e.__traceback__])
+
+
+def create_nested_eg():
+    excs = []
+    try:
+        try:
+            raise TypeError(bytes)
+        except TypeError as e:
+            raise ExceptionGroup("nested", [e])
+    except ExceptionGroup as e:
+        excs.append(e)
+
+    try:
+        try:
+            raise MemoryError('out of memory')
+        except MemoryError as e:
+            raise ValueError(1) from e
+    except ValueError as e:
+        excs.append(e)
+
+    try:
+        raise ExceptionGroup("root", excs)
+    except ExceptionGroup as eg:
+        return eg
+
+
+class NestedExceptionGroupBasicsTest(ExceptionGroupTestBase):
+    def test_nested_group_matches_template(self):
+        eg = create_nested_eg()
+        self.assertMatchesTemplate(
+            eg,
+            ExceptionGroup,
+            [[TypeError(bytes)], ValueError(1)])
+
+    def test_nested_group_chaining(self):
+        eg = create_nested_eg()
+        self.assertIsInstance(eg.exceptions[1].__context__, MemoryError)
+        self.assertIsInstance(eg.exceptions[1].__cause__, MemoryError)
+        self.assertIsInstance(eg.exceptions[0].__context__, TypeError)
+
+    def test_nested_exception_group_tracebacks(self):
+        eg = create_nested_eg()
+
+        line0 = create_nested_eg.__code__.co_firstlineno
+        for (tb, expected) in [
+            (eg.__traceback__, line0 + 19),
+            (eg.exceptions[0].__traceback__, line0 + 6),
+            (eg.exceptions[1].__traceback__, line0 + 14),
+            (eg.exceptions[0].exceptions[0].__traceback__, line0 + 4),
+        ]:
+            self.assertEqual(tb.tb_lineno, expected)
+            self.assertIsNone(tb.tb_next)
+
+    def test_iteration_full_tracebacks(self):
+        eg = create_nested_eg()
+        # check that iteration over leaves
+        # produces the expected tracebacks
+        self.assertEqual(len(list(leaf_generator(eg))), 2)
+
+        line0 = create_nested_eg.__code__.co_firstlineno
+        expected_tbs = [ [line0 + 19, line0 + 6, line0 + 4],
+                         [line0 + 19, line0 + 14]]
+
+        for (i, (_, tbs)) in enumerate(leaf_generator(eg)):
+            self.assertSequenceEqual(
+                [tb.tb_lineno for tb in tbs],
+                expected_tbs[i])
+
+
+class ExceptionGroupSplitTestBase(ExceptionGroupTestBase):
+
+    def split_exception_group(self, eg, types):
+        """ Split an EG and do some sanity checks on the result """
+        self.assertIsInstance(eg, BaseExceptionGroup)
+
+        match, rest = eg.split(types)
+        sg = eg.subgroup(types)
+
+        if match is not None:
+            self.assertIsInstance(match, BaseExceptionGroup)
+            for e,_ in leaf_generator(match):
+                self.assertIsInstance(e, types)
+
+            self.assertIsNotNone(sg)
+            self.assertIsInstance(sg, BaseExceptionGroup)
+            for e,_ in leaf_generator(sg):
+                self.assertIsInstance(e, types)
+
+        if rest is not None:
+            self.assertIsInstance(rest, BaseExceptionGroup)
+
+        def leaves(exc):
+            return [] if exc is None else [e for e,_ in leaf_generator(exc)]
+
+        # match and subgroup have the same leaves
+        self.assertSequenceEqual(leaves(match), leaves(sg))
+
+        match_leaves = leaves(match)
+        rest_leaves = leaves(rest)
+        # each leaf exception of eg is in exactly one of match and rest
+        self.assertEqual(
+            len(leaves(eg)),
+            len(leaves(match)) + len(leaves(rest)))
+
+        for e in leaves(eg):
+            self.assertNotEqual(
+                match and e in match_leaves,
+                rest and e in rest_leaves)
+
+        # message, cause and context equal to eg
+        for part in [match, rest, sg]:
+            if part is not None:
+                self.assertEqual(eg.message, part.message)
+                self.assertIs(eg.__cause__, part.__cause__)
+                self.assertIs(eg.__context__, part.__context__)
+                self.assertIs(eg.__traceback__, part.__traceback__)
+
+        def tbs_for_leaf(leaf, eg):
+            for e, tbs in leaf_generator(eg):
+                if e is leaf:
+                    return tbs
+
+        def tb_linenos(tbs):
+            return [tb.tb_lineno for tb in tbs if tb]
+
+        # full tracebacks match
+        for part in [match, rest, sg]:
+            for e in leaves(part):
+                self.assertSequenceEqual(
+                    tb_linenos(tbs_for_leaf(e, eg)),
+                    tb_linenos(tbs_for_leaf(e, part)))
+
+        return match, rest
+
+
+class NestedExceptionGroupSplitTest(ExceptionGroupSplitTestBase):
+
+    def test_split_by_type(self):
+        class MyExceptionGroup(ExceptionGroup):
+            pass
+
+        def raiseVE(v):
+            raise ValueError(v)
+
+        def raiseTE(t):
+            raise TypeError(t)
+
+        def nested_group():
+            def level1(i):
+                excs = []
+                for f, arg in [(raiseVE, i), (raiseTE, int), (raiseVE, i+1)]:
+                    try:
+                        f(arg)
+                    except Exception as e:
+                        excs.append(e)
+                raise ExceptionGroup('msg1', excs)
+
+            def level2(i):
+                excs = []
+                for f, arg in [(level1, i), (level1, i+1), (raiseVE, i+2)]:
+                    try:
+                        f(arg)
+                    except Exception as e:
+                        excs.append(e)
+                raise MyExceptionGroup('msg2', excs)
+
+            def level3(i):
+                excs = []
+                for f, arg in [(level2, i+1), (raiseVE, i+2)]:
+                    try:
+                        f(arg)
+                    except Exception as e:
+                        excs.append(e)
+                raise ExceptionGroup('msg3', excs)
+
+            level3(5)
+
+        try:
+            nested_group()
+        except ExceptionGroup as e:
+            eg = e
+
+        eg_template = [
+            [
+                [ValueError(6), TypeError(int), ValueError(7)],
+                [ValueError(7), TypeError(int), ValueError(8)],
+                ValueError(8),
+            ],
+            ValueError(7)]
+
+        valueErrors_template = [
+            [
+                [ValueError(6), ValueError(7)],
+                [ValueError(7), ValueError(8)],
+                ValueError(8),
+            ],
+            ValueError(7)]
+
+        typeErrors_template = [[[TypeError(int)], [TypeError(int)]]]
+
+        self.assertMatchesTemplate(eg, ExceptionGroup, eg_template)
+
+        # Match Nothing
+        match, rest = self.split_exception_group(eg, SyntaxError)
+        self.assertIsNone(match)
+        self.assertMatchesTemplate(rest, ExceptionGroup, eg_template)
+
+        # Match Everything
+        match, rest = self.split_exception_group(eg, BaseException)
+        self.assertMatchesTemplate(match, ExceptionGroup, eg_template)
+        self.assertIsNone(rest)
+        match, rest = self.split_exception_group(eg, (ValueError, TypeError))
+        self.assertMatchesTemplate(match, ExceptionGroup, eg_template)
+        self.assertIsNone(rest)
+
+        # Match ValueErrors
+        match, rest = self.split_exception_group(eg, ValueError)
+        self.assertMatchesTemplate(match, ExceptionGroup, valueErrors_template)
+        self.assertMatchesTemplate(rest, ExceptionGroup, typeErrors_template)
+
+        # Match TypeErrors
+        match, rest = self.split_exception_group(eg, (TypeError, SyntaxError))
+        self.assertMatchesTemplate(match, ExceptionGroup, typeErrors_template)
+        self.assertMatchesTemplate(rest, ExceptionGroup, valueErrors_template)
+
+        # Match ExceptionGroup
+        match, rest = eg.split(ExceptionGroup)
+        self.assertIs(match, eg)
+        self.assertIsNone(rest)
+
+        # Match MyExceptionGroup (ExceptionGroup subclass)
+        match, rest = eg.split(MyExceptionGroup)
+        self.assertMatchesTemplate(match, ExceptionGroup, [eg_template[0]])
+        self.assertMatchesTemplate(rest, ExceptionGroup, [eg_template[1]])
+
+    def test_split_BaseExceptionGroup(self):
+        def exc(ex):
+            try:
+                raise ex
+            except BaseException as e:
+                return e
+
+        try:
+            raise BaseExceptionGroup(
+                "beg", [exc(ValueError(1)), exc(KeyboardInterrupt(2))])
+        except BaseExceptionGroup as e:
+            beg = e
+
+        # Match Nothing
+        match, rest = self.split_exception_group(beg, TypeError)
+        self.assertIsNone(match)
+        self.assertMatchesTemplate(
+            rest, BaseExceptionGroup, [ValueError(1), KeyboardInterrupt(2)])
+
+        # Match Everything
+        match, rest = self.split_exception_group(
+            beg, (ValueError, KeyboardInterrupt))
+        self.assertMatchesTemplate(
+            match, BaseExceptionGroup, [ValueError(1), KeyboardInterrupt(2)])
+        self.assertIsNone(rest)
+
+        # Match ValueErrors
+        match, rest = self.split_exception_group(beg, ValueError)
+        self.assertMatchesTemplate(
+            match, ExceptionGroup, [ValueError(1)])
+        self.assertMatchesTemplate(
+            rest, BaseExceptionGroup, [KeyboardInterrupt(2)])
+
+        # Match KeyboardInterrupts
+        match, rest = self.split_exception_group(beg, KeyboardInterrupt)
+        self.assertMatchesTemplate(
+            match, BaseExceptionGroup, [KeyboardInterrupt(2)])
+        self.assertMatchesTemplate(
+            rest, ExceptionGroup, [ValueError(1)])
+
+
+class NestedExceptionGroupSubclassSplitTest(ExceptionGroupSplitTestBase):
+
+    def test_split_ExceptionGroup_subclass_no_derive_no_new_override(self):
+        class EG(ExceptionGroup):
+            pass
+
+        try:
+            try:
+                try:
+                    raise TypeError(2)
+                except TypeError as te:
+                    raise EG("nested", [te])
+            except EG as nested:
+                try:
+                    raise ValueError(1)
+                except ValueError as ve:
+                    raise EG("eg", [ve, nested])
+        except EG as e:
+            eg = e
+
+        self.assertMatchesTemplate(eg, EG, [ValueError(1), [TypeError(2)]])
+
+        # Match Nothing
+        match, rest = self.split_exception_group(eg, OSError)
+        self.assertIsNone(match)
+        self.assertMatchesTemplate(
+            rest, ExceptionGroup, [ValueError(1), [TypeError(2)]])
+
+        # Match Everything
+        match, rest = self.split_exception_group(eg, (ValueError, TypeError))
+        self.assertMatchesTemplate(
+            match, ExceptionGroup, [ValueError(1), [TypeError(2)]])
+        self.assertIsNone(rest)
+
+        # Match ValueErrors
+        match, rest = self.split_exception_group(eg, ValueError)
+        self.assertMatchesTemplate(match, ExceptionGroup, [ValueError(1)])
+        self.assertMatchesTemplate(rest, ExceptionGroup, [[TypeError(2)]])
+
+        # Match TypeErrors
+        match, rest = self.split_exception_group(eg, TypeError)
+        self.assertMatchesTemplate(match, ExceptionGroup, [[TypeError(2)]])
+        self.assertMatchesTemplate(rest, ExceptionGroup, [ValueError(1)])
+
+    def test_split_BaseExceptionGroup_subclass_no_derive_new_override(self):
+        class EG(BaseExceptionGroup):
+            def __new__(cls, message, excs, unused):
+                # The "unused" arg is here to show that split() doesn't call
+                # the actual class constructor from the default derive()
+                # implementation (it would fail on unused arg if so because
+                # it assumes the BaseExceptionGroup.__new__ signature).
+                return super().__new__(cls, message, excs)
+
+        try:
+            raise EG("eg", [ValueError(1), KeyboardInterrupt(2)], "unused")
+        except EG as e:
+            eg = e
+
+        self.assertMatchesTemplate(
+            eg, EG, [ValueError(1), KeyboardInterrupt(2)])
+
+        # Match Nothing
+        match, rest = self.split_exception_group(eg, OSError)
+        self.assertIsNone(match)
+        self.assertMatchesTemplate(
+            rest, BaseExceptionGroup, [ValueError(1), KeyboardInterrupt(2)])
+
+        # Match Everything
+        match, rest = self.split_exception_group(
+            eg, (ValueError, KeyboardInterrupt))
+        self.assertMatchesTemplate(
+            match, BaseExceptionGroup, [ValueError(1), KeyboardInterrupt(2)])
+        self.assertIsNone(rest)
+
+        # Match ValueErrors
+        match, rest = self.split_exception_group(eg, ValueError)
+        self.assertMatchesTemplate(match, ExceptionGroup, [ValueError(1)])
+        self.assertMatchesTemplate(
+            rest, BaseExceptionGroup, [KeyboardInterrupt(2)])
+
+        # Match KeyboardInterrupt
+        match, rest = self.split_exception_group(eg, KeyboardInterrupt)
+        self.assertMatchesTemplate(
+            match, BaseExceptionGroup, [KeyboardInterrupt(2)])
+        self.assertMatchesTemplate(rest, ExceptionGroup, [ValueError(1)])
+
+    def test_split_ExceptionGroup_subclass_derive_and_new_overrides(self):
+        class EG(ExceptionGroup):
+            def __new__(cls, message, excs, code):
+                obj = super().__new__(cls, message, excs)
+                obj.code = code
+                return obj
+
+            def derive(self, excs):
+                return EG(self.message, excs, self.code)
+
+        try:
+            try:
+                try:
+                    raise TypeError(2)
+                except TypeError as te:
+                    raise EG("nested", [te], 101)
+            except EG as nested:
+                try:
+                    raise ValueError(1)
+                except ValueError as ve:
+                    raise EG("eg", [ve, nested], 42)
+        except EG as e:
+            eg = e
+
+        self.assertMatchesTemplate(eg, EG, [ValueError(1), [TypeError(2)]])
+
+        # Match Nothing
+        match, rest = self.split_exception_group(eg, OSError)
+        self.assertIsNone(match)
+        self.assertMatchesTemplate(rest, EG, [ValueError(1), [TypeError(2)]])
+        self.assertEqual(rest.code, 42)
+        self.assertEqual(rest.exceptions[1].code, 101)
+
+        # Match Everything
+        match, rest = self.split_exception_group(eg, (ValueError, TypeError))
+        self.assertMatchesTemplate(match, EG, [ValueError(1), [TypeError(2)]])
+        self.assertEqual(match.code, 42)
+        self.assertEqual(match.exceptions[1].code, 101)
+        self.assertIsNone(rest)
+
+        # Match ValueErrors
+        match, rest = self.split_exception_group(eg, ValueError)
+        self.assertMatchesTemplate(match, EG, [ValueError(1)])
+        self.assertEqual(match.code, 42)
+        self.assertMatchesTemplate(rest, EG, [[TypeError(2)]])
+        self.assertEqual(rest.code, 42)
+        self.assertEqual(rest.exceptions[0].code, 101)
+
+        # Match TypeErrors
+        match, rest = self.split_exception_group(eg, TypeError)
+        self.assertMatchesTemplate(match, EG, [[TypeError(2)]])
+        self.assertEqual(match.code, 42)
+        self.assertEqual(match.exceptions[0].code, 101)
+        self.assertMatchesTemplate(rest, EG, [ValueError(1)])
+        self.assertEqual(rest.code, 42)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Lib/test/test_pickle.py b/Lib/test/test_pickle.py
index 8775ff4b79157..057af21e71fe4 100644
--- a/Lib/test/test_pickle.py
+++ b/Lib/test/test_pickle.py
@@ -489,7 +489,9 @@ def test_exceptions(self):
                            ResourceWarning,
                            StopAsyncIteration,
                            RecursionError,
-                           EncodingWarning):
+                           EncodingWarning,
+                           BaseExceptionGroup,
+                           ExceptionGroup):
                     continue
                 if exc is not OSError and issubclass(exc, OSError):
                     self.assertEqual(reverse_mapping('builtins', name),
diff --git a/Lib/test/test_stable_abi_ctypes.py b/Lib/test/test_stable_abi_ctypes.py
index 750aa18108327..1e27bcaf889a2 100644
--- a/Lib/test/test_stable_abi_ctypes.py
+++ b/Lib/test/test_stable_abi_ctypes.py
@@ -198,6 +198,7 @@ def test_available_symbols(self):
     "PyExc_AssertionError",
     "PyExc_AttributeError",
     "PyExc_BaseException",
+    "PyExc_BaseExceptionGroup",
     "PyExc_BlockingIOError",
     "PyExc_BrokenPipeError",
     "PyExc_BufferError",
diff --git a/Misc/NEWS.d/next/Core and Builtins/2021-09-26-18-18-50.bpo-45292.aX5HVr.rst b/Misc/NEWS.d/next/Core and Builtins/2021-09-26-18-18-50.bpo-45292.aX5HVr.rst
new file mode 100644
index 0000000000000..ee48b6d5105c5
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2021-09-26-18-18-50.bpo-45292.aX5HVr.rst	
@@ -0,0 +1 @@
+Implement :pep:`654` Add :class:`ExceptionGroup` and :class:`BaseExceptionGroup`.
diff --git a/Misc/stable_abi.txt b/Misc/stable_abi.txt
index 23e5b96a0e8a7..9f5a85bdec40f 100644
--- a/Misc/stable_abi.txt
+++ b/Misc/stable_abi.txt
@@ -619,6 +619,8 @@ data PyExc_AttributeError
     added 3.2
 data PyExc_BaseException
     added 3.2
+data PyExc_BaseExceptionGroup
+    added 3.11
 data PyExc_BufferError
     added 3.2
 data PyExc_BytesWarning
diff --git a/Objects/exceptions.c b/Objects/exceptions.c
index a9ea42c98422d..a5459da89a073 100644
--- a/Objects/exceptions.c
+++ b/Objects/exceptions.c
@@ -6,6 +6,7 @@
 
 #define PY_SSIZE_T_CLEAN
 #include <Python.h>
+#include <stdbool.h>
 #include "pycore_initconfig.h"
 #include "pycore_object.h"
 #include "structmember.h"         // PyMemberDef
@@ -540,7 +541,7 @@ StopIteration_clear(PyStopIterationObject *self)
 static void
 StopIteration_dealloc(PyStopIterationObject *self)
 {
-    _PyObject_GC_UNTRACK(self);
+    PyObject_GC_UnTrack(self);
     StopIteration_clear(self);
     Py_TYPE(self)->tp_free((PyObject *)self);
 }
@@ -629,6 +630,516 @@ ComplexExtendsException(PyExc_BaseException, SystemExit, SystemExit,
                         0, 0, SystemExit_members, 0, 0,
                         "Request to exit from the interpreter.");
 
+/*
+ *    BaseExceptionGroup extends BaseException
+ *    ExceptionGroup extends BaseExceptionGroup and Exception
+ */
+
+
+static inline PyBaseExceptionGroupObject*
+_PyBaseExceptionGroupObject_cast(PyObject *exc)
+{
+    assert(_PyBaseExceptionGroup_Check(exc));
+    return (PyBaseExceptionGroupObject *)exc;
+}
+
+static PyObject *
+BaseExceptionGroup_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+{
+    struct _Py_exc_state *state = get_exc_state();
+    PyTypeObject *PyExc_ExceptionGroup =
+        (PyTypeObject*)state->PyExc_ExceptionGroup;
+
+    PyObject *message = NULL;
+    PyObject *exceptions = NULL;
+
+    if (!PyArg_ParseTuple(args, "UO", &message, &exceptions)) {
+        return NULL;
+    }
+
+    if (!PySequence_Check(exceptions)) {
+        PyErr_SetString(
+            PyExc_TypeError,
+            "second argument (exceptions) must be a sequence");
+        return NULL;
+    }
+
+    exceptions = PySequence_Tuple(exceptions);
+    if (!exceptions) {
+        return NULL;
+    }
+
+    /* We are now holding a ref to the exceptions tuple */
+
+    Py_ssize_t numexcs = PyTuple_GET_SIZE(exceptions);
+    if (numexcs == 0) {
+        PyErr_SetString(
+            PyExc_ValueError,
+            "second argument (exceptions) must be a non-empty sequence");
+        goto error;
+    }
+
+    bool nested_base_exceptions = false;
+    for (Py_ssize_t i = 0; i < numexcs; i++) {
+        PyObject *exc = PyTuple_GET_ITEM(exceptions, i);
+        if (!exc) {
+            goto error;
+        }
+        if (!PyExceptionInstance_Check(exc)) {
+            PyErr_Format(
+                PyExc_ValueError,
+                "Item %d of second argument (exceptions) is not an exception",
+                i);
+            goto error;
+        }
+        int is_nonbase_exception = PyObject_IsInstance(exc, PyExc_Exception);
+        if (is_nonbase_exception < 0) {
+            goto error;
+        }
+        else if (is_nonbase_exception == 0) {
+            nested_base_exceptions = true;
+        }
+    }
+
+    PyTypeObject *cls = type;
+    if (cls == PyExc_ExceptionGroup) {
+        if (nested_base_exceptions) {
+            PyErr_SetString(PyExc_TypeError,
+                "Cannot nest BaseExceptions in an ExceptionGroup");
+            goto error;
+        }
+    }
+    else if (cls == (PyTypeObject*)PyExc_BaseExceptionGroup) {
+        if (!nested_base_exceptions) {
+            /* All nested exceptions are Exception subclasses,
+             * wrap them in an ExceptionGroup
+             */
+            cls = PyExc_ExceptionGroup;
+        }
+    }
+    else {
+        /* Do nothing - we don't interfere with subclasses */
+    }
+
+    if (!cls) {
+        /* Don't crash during interpreter shutdown
+         * (PyExc_ExceptionGroup may have been cleared)
+         */
+        cls = (PyTypeObject*)PyExc_BaseExceptionGroup;
+    }
+    PyBaseExceptionGroupObject *self =
+        _PyBaseExceptionGroupObject_cast(BaseException_new(cls, args, kwds));
+    if (!self) {
+        goto error;
+    }
+
+    self->msg = Py_NewRef(message);
+    self->excs = exceptions;
+    return (PyObject*)self;
+error:
+    Py_DECREF(exceptions);
+    return NULL;
+}
+
+static int
+BaseExceptionGroup_init(PyBaseExceptionGroupObject *self,
+    PyObject *args, PyObject *kwds)
+{
+    if (!_PyArg_NoKeywords(Py_TYPE(self)->tp_name, kwds)) {
+        return -1;
+    }
+    if (BaseException_init((PyBaseExceptionObject *)self, args, kwds) == -1) {
+        return -1;
+    }
+    return 0;
+}
+
+static int
+BaseExceptionGroup_clear(PyBaseExceptionGroupObject *self)
+{
+    Py_CLEAR(self->msg);
+    Py_CLEAR(self->excs);
+    return BaseException_clear((PyBaseExceptionObject *)self);
+}
+
+static void
+BaseExceptionGroup_dealloc(PyBaseExceptionGroupObject *self)
+{
+    _PyObject_GC_UNTRACK(self);
+    BaseExceptionGroup_clear(self);
+    Py_TYPE(self)->tp_free((PyObject *)self);
+}
+
+static int
+BaseExceptionGroup_traverse(PyBaseExceptionGroupObject *self,
+     visitproc visit, void *arg)
+{
+    Py_VISIT(self->msg);
+    Py_VISIT(self->excs);
+    return BaseException_traverse((PyBaseExceptionObject *)self, visit, arg);
+}
+
+static PyObject *
+BaseExceptionGroup_str(PyBaseExceptionGroupObject *self)
+{
+    assert(self->msg);
+    assert(PyUnicode_Check(self->msg));
+    return Py_NewRef(self->msg);
+}
+
+static PyObject *
+BaseExceptionGroup_derive(PyObject *self_, PyObject *args)
+{
+    PyBaseExceptionGroupObject *self = _PyBaseExceptionGroupObject_cast(self_);
+    PyObject *excs = NULL;
+    if (!PyArg_ParseTuple(args, "O", &excs)) {
+        return NULL;
+    }
+    PyObject *init_args = PyTuple_Pack(2, self->msg, excs);
+    if (!init_args) {
+        return NULL;
+    }
+    PyObject *eg = PyObject_CallObject(
+        PyExc_BaseExceptionGroup, init_args);
+    Py_DECREF(init_args);
+    return eg;
+}
+
+static int
+exceptiongroup_subset(
+    PyBaseExceptionGroupObject *_orig, PyObject *excs, PyObject **result)
+{
+    /* Sets *result to an ExceptionGroup wrapping excs with metadata from
+     * _orig. If excs is empty, sets *result to NULL.
+     * Returns 0 on success and -1 on error.
+
+     * This function is used by split() to construct the match/rest parts,
+     * so excs is the matching or non-matching sub-sequence of orig->excs
+     * (this function does not verify that it is a subsequence).
+     */
+    PyObject *orig = (PyObject *)_orig;
+
+    *result = NULL;
+    Py_ssize_t num_excs = PySequence_Size(excs);
+    if (num_excs < 0) {
+        return -1;
+    }
+    else if (num_excs == 0) {
+        return 0;
+    }
+
+    PyObject *eg = PyObject_CallMethod(
+        orig, "derive", "(O)", excs);
+    if (!eg) {
+        return -1;
+    }
+
+    if (!_PyBaseExceptionGroup_Check(eg)) {
+        PyErr_SetString(PyExc_TypeError,
+            "derive must return an instance of BaseExceptionGroup");
+        goto error;
+    }
+
+    /* Now we hold a reference to the new eg */
+
+    PyObject *tb = PyException_GetTraceback(orig);
+    if (tb) {
+        int res = PyException_SetTraceback(eg, tb);
+        Py_DECREF(tb);
+        if (res == -1) {
+            goto error;
+        }
+    }
+    PyException_SetContext(eg, PyException_GetContext(orig));
+    PyException_SetCause(eg, PyException_GetCause(orig));
+    *result = eg;
+    return 0;
+error:
+    Py_DECREF(eg);
+    return -1;
+}
+
+typedef enum {
+    /* Exception type or tuple of thereof */
+    EXCEPTION_GROUP_MATCH_BY_TYPE = 0,
+    /* A PyFunction returning True for matching exceptions */
+    EXCEPTION_GROUP_MATCH_BY_PREDICATE = 1,
+    /* An instance or container thereof, checked with equality
+     * This matcher type is only used internally by the
+     * interpreter, it is not exposed to python code */
+    EXCEPTION_GROUP_MATCH_INSTANCES = 2
+} _exceptiongroup_split_matcher_type;
+
+static int
+get_matcher_type(PyObject *value,
+                  _exceptiongroup_split_matcher_type *type)
+{
+    /* the python API supports only BY_TYPE and BY_PREDICATE */
+    if (PyExceptionClass_Check(value) ||
+        PyTuple_CheckExact(value)) {
+        *type = EXCEPTION_GROUP_MATCH_BY_TYPE;
+        return 0;
+    }
+    if (PyFunction_Check(value)) {
+        *type = EXCEPTION_GROUP_MATCH_BY_PREDICATE;
+        return 0;
+    }
+    PyErr_SetString(
+        PyExc_TypeError,
+        "expected a function, exception type or tuple of exception types");
+    return -1;
+}
+
+static int
+exceptiongroup_split_check_match(PyObject *exc,
+                                 _exceptiongroup_split_matcher_type matcher_type,
+                                 PyObject *matcher_value)
+{
+    switch (matcher_type) {
+    case EXCEPTION_GROUP_MATCH_BY_TYPE: {
+        assert(PyExceptionClass_Check(matcher_value) ||
+               PyTuple_CheckExact(matcher_value));
+        return PyErr_GivenExceptionMatches(exc, matcher_value);
+    }
+    case EXCEPTION_GROUP_MATCH_BY_PREDICATE: {
+        assert(PyFunction_Check(matcher_value));
+        PyObject *exc_matches = PyObject_CallOneArg(matcher_value, exc);
+        if (exc_matches == NULL) {
+            return -1;
+        }
+        int is_true = PyObject_IsTrue(exc_matches);
+        Py_DECREF(exc_matches);
+        return is_true;
+    }
+    case EXCEPTION_GROUP_MATCH_INSTANCES: {
+        if (PySequence_Check(matcher_value)) {
+            return PySequence_Contains(matcher_value, exc);
+        }
+        return matcher_value == exc;
+    }
+    }
+    return 0;
+}
+
+typedef struct {
+    PyObject *match;
+    PyObject *rest;
+} _exceptiongroup_split_result;
+
+static int
+exceptiongroup_split_recursive(PyObject *exc,
+                               _exceptiongroup_split_matcher_type matcher_type,
+                               PyObject *matcher_value,
+                               bool construct_rest,
+                               _exceptiongroup_split_result *result)
+{
+    result->match = NULL;
+    result->rest = NULL;
+
+    int is_match = exceptiongroup_split_check_match(
+        exc, matcher_type, matcher_value);
+    if (is_match < 0) {
+        return -1;
+    }
+
+    if (is_match) {
+        /* Full match */
+        result->match = Py_NewRef(exc);
+        return 0;
+    }
+    else if (!_PyBaseExceptionGroup_Check(exc)) {
+        /* Leaf exception and no match */
+        if (construct_rest) {
+            result->rest = Py_NewRef(exc);
+        }
+        return 0;
+    }
+
+    /* Partial match */
+
+    PyBaseExceptionGroupObject *eg = _PyBaseExceptionGroupObject_cast(exc);
+    assert(PyTuple_CheckExact(eg->excs));
+    Py_ssize_t num_excs = PyTuple_Size(eg->excs);
+    if (num_excs < 0) {
+        return -1;
+    }
+    assert(num_excs > 0); /* checked in constructor, and excs is read-only */
+
+    int retval = -1;
+    PyObject *match_list = PyList_New(0);
+    if (!match_list) {
+        return -1;
+    }
+
+    PyObject *rest_list = NULL;
+    if (construct_rest) {
+        rest_list = PyList_New(0);
+        if (!rest_list) {
+            goto done;
+        }
+    }
+    /* recursive calls */
+    for (Py_ssize_t i = 0; i < num_excs; i++) {
+        PyObject *e = PyTuple_GET_ITEM(eg->excs, i);
+        _exceptiongroup_split_result rec_result;
+        if (Py_EnterRecursiveCall(" in exceptiongroup_split_recursive")) {
+            goto done;
+        }
+        if (exceptiongroup_split_recursive(
+                e, matcher_type, matcher_value,
+                construct_rest, &rec_result) == -1) {
+            assert(!rec_result.match);
+            assert(!rec_result.rest);
+            Py_LeaveRecursiveCall();
+            goto done;
+        }
+        Py_LeaveRecursiveCall();
+        if (rec_result.match) {
+            assert(PyList_CheckExact(match_list));
+            if (PyList_Append(match_list, rec_result.match) == -1) {
+                Py_DECREF(rec_result.match);
+                goto done;
+            }
+            Py_DECREF(rec_result.match);
+        }
+        if (rec_result.rest) {
+            assert(construct_rest);
+            assert(PyList_CheckExact(rest_list));
+            if (PyList_Append(rest_list, rec_result.rest) == -1) {
+                Py_DECREF(rec_result.rest);
+                goto done;
+            }
+            Py_DECREF(rec_result.rest);
+        }
+    }
+
+    /* construct result */
+    if (exceptiongroup_subset(eg, match_list, &result->match) == -1) {
+        goto done;
+    }
+
+    if (construct_rest) {
+        assert(PyList_CheckExact(rest_list));
+        if (exceptiongroup_subset(eg, rest_list, &result->rest) == -1) {
+            Py_CLEAR(result->match);
+            goto done;
+        }
+    }
+    retval = 0;
+done:
+    Py_DECREF(match_list);
+    Py_XDECREF(rest_list);
+    if (retval == -1) {
+        Py_CLEAR(result->match);
+        Py_CLEAR(result->rest);
+    }
+    return retval;
+}
+
+static PyObject *
+BaseExceptionGroup_split(PyObject *self, PyObject *args)
+{
+    PyObject *matcher_value = NULL;
+    if (!PyArg_UnpackTuple(args, "split", 1, 1, &matcher_value)) {
+        return NULL;
+    }
+
+    _exceptiongroup_split_matcher_type matcher_type;
+    if (get_matcher_type(matcher_value, &matcher_type) == -1) {
+        return NULL;
+    }
+
+    _exceptiongroup_split_result split_result;
+    bool construct_rest = true;
+    if (exceptiongroup_split_recursive(
+            self, matcher_type, matcher_value,
+            construct_rest, &split_result) == -1) {
+        return NULL;
+    }
+
+    PyObject *result = PyTuple_Pack(
+            2,
+            split_result.match ? split_result.match : Py_None,
+            split_result.rest ? split_result.rest : Py_None);
+
+    Py_XDECREF(split_result.match);
+    Py_XDECREF(split_result.rest);
+    return result;
+}
+
+static PyObject *
+BaseExceptionGroup_subgroup(PyObject *self, PyObject *args)
+{
+    PyObject *matcher_value = NULL;
+    if (!PyArg_UnpackTuple(args, "subgroup", 1, 1, &matcher_value)) {
+        return NULL;
+    }
+
+    _exceptiongroup_split_matcher_type matcher_type;
+    if (get_matcher_type(matcher_value, &matcher_type) == -1) {
+        return NULL;
+    }
+
+    _exceptiongroup_split_result split_result;
+    bool construct_rest = false;
+    if (exceptiongroup_split_recursive(
+            self, matcher_type, matcher_value,
+            construct_rest, &split_result) == -1) {
+        return NULL;
+    }
+
+    PyObject *result = Py_NewRef(
+            split_result.match ? split_result.match : Py_None);
+
+    Py_XDECREF(split_result.match);
+    assert(!split_result.rest);
+    return result;
+}
+
+static PyMemberDef BaseExceptionGroup_members[] = {
+    {"message", T_OBJECT, offsetof(PyBaseExceptionGroupObject, msg), READONLY,
+        PyDoc_STR("exception message")},
+    {"exceptions", T_OBJECT, offsetof(PyBaseExceptionGroupObject, excs), READONLY,
+        PyDoc_STR("nested exceptions")},
+    {NULL}  /* Sentinel */
+};
+
+static PyMethodDef BaseExceptionGroup_methods[] = {
+    {"__class_getitem__", (PyCFunction)Py_GenericAlias,
+      METH_O|METH_CLASS, PyDoc_STR("See PEP 585")},
+    {"derive", (PyCFunction)BaseExceptionGroup_derive, METH_VARARGS},
+    {"split", (PyCFunction)BaseExceptionGroup_split, METH_VARARGS},
+    {"subgroup", (PyCFunction)BaseExceptionGroup_subgroup, METH_VARARGS},
+    {NULL}
+};
+
+ComplexExtendsException(PyExc_BaseException, BaseExceptionGroup,
+    BaseExceptionGroup, BaseExceptionGroup_new /* new */,
+    BaseExceptionGroup_methods, BaseExceptionGroup_members,
+    0 /* getset */, BaseExceptionGroup_str,
+    "A combination of multiple unrelated exceptions.");
+
+/*
+ *    ExceptionGroup extends BaseExceptionGroup, Exception
+ */
+static PyObject*
+create_exception_group_class(void) {
+    struct _Py_exc_state *state = get_exc_state();
+
+    PyObject *bases = PyTuple_Pack(
+        2, PyExc_BaseExceptionGroup, PyExc_Exception);
+    if (bases == NULL) {
+        return NULL;
+    }
+
+    assert(!state->PyExc_ExceptionGroup);
+    state->PyExc_ExceptionGroup = PyErr_NewException(
+        "builtins.ExceptionGroup", bases, NULL);
+
+    Py_DECREF(bases);
+    return state->PyExc_ExceptionGroup;
+}
+
 /*
  *    KeyboardInterrupt extends BaseException
  */
@@ -2671,6 +3182,7 @@ _PyExc_Init(PyInterpreterState *interp)
     } while (0)
 
     PRE_INIT(BaseException);
+    PRE_INIT(BaseExceptionGroup);
     PRE_INIT(Exception);
     PRE_INIT(TypeError);
     PRE_INIT(StopAsyncIteration);
@@ -2805,8 +3317,15 @@ _PyBuiltins_AddExceptions(PyObject *bltinmod)
         return _PyStatus_ERR("exceptions bootstrapping error.");
     }
 
+    PyObject *PyExc_ExceptionGroup = create_exception_group_class();
+    if (!PyExc_ExceptionGroup) {
+        return _PyStatus_ERR("exceptions bootstrapping error.");
+    }
+
     POST_INIT(BaseException);
     POST_INIT(Exception);
+    POST_INIT(BaseExceptionGroup);
+    POST_INIT(ExceptionGroup);
     POST_INIT(TypeError);
     POST_INIT(StopAsyncIteration);
     POST_INIT(StopIteration);
@@ -2885,6 +3404,13 @@ _PyBuiltins_AddExceptions(PyObject *bltinmod)
 #undef INIT_ALIAS
 }
 
+void
+_PyExc_ClearExceptionGroupType(PyInterpreterState *interp)
+{
+    struct _Py_exc_state *state = &interp->exc_state;
+    Py_CLEAR(state->PyExc_ExceptionGroup);
+}
+
 void
 _PyExc_Fini(PyInterpreterState *interp)
 {
diff --git a/PC/python3dll.c b/PC/python3dll.c
index d9e6fd3e7ca7c..d2a87070de5cc 100755
--- a/PC/python3dll.c
+++ b/PC/python3dll.c
@@ -754,6 +754,7 @@ EXPORT_DATA(PyExc_ArithmeticError)
 EXPORT_DATA(PyExc_AssertionError)
 EXPORT_DATA(PyExc_AttributeError)
 EXPORT_DATA(PyExc_BaseException)
+EXPORT_DATA(PyExc_BaseExceptionGroup)
 EXPORT_DATA(PyExc_BlockingIOError)
 EXPORT_DATA(PyExc_BrokenPipeError)
 EXPORT_DATA(PyExc_BufferError)
diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c
index c5a209abae61a..9ce845ca61d21 100644
--- a/Python/pylifecycle.c
+++ b/Python/pylifecycle.c
@@ -1662,6 +1662,8 @@ finalize_interp_clear(PyThreadState *tstate)
 {
     int is_main_interp = _Py_IsMainInterpreter(tstate->interp);
 
+    _PyExc_ClearExceptionGroupType(tstate->interp);
+
     /* Clear interpreter state and all thread states */
     _PyInterpreterState_Clear(tstate);
 



More information about the Python-checkins mailing list