gh-135736: call exception handler for all BaseExceptions in asyncio.TaskGroup (#154538)

kumaraditya303 <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/a7b2e1f4e07718e1e047dd7683b488e239688ceb
commit: a7b2e1f4e07718e1e047dd7683b488e239688ceb
branch: main
author: Sreehari Annam <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-08-14T20:43:19+05:30
summary:

gh-135736: call exception handler for all BaseExceptions in asyncio.TaskGroup (#154538)

files:
A Misc/NEWS.d/next/Library/2026-07-23-12-27-52.gh-issue-135736.hkuGvu.rst
M Lib/asyncio/taskgroups.py
M Lib/test/test_asyncio/test_taskgroups.py

diff --git a/Lib/asyncio/taskgroups.py b/Lib/asyncio/taskgroups.py
index a431b45a19489d5..1b77d6d317e9375 100644
--- a/Lib/asyncio/taskgroups.py
+++ b/Lib/asyncio/taskgroups.py
@@ -140,6 +140,18 @@ async def _aexit(self, et, exc):
         assert not self._tasks
 
         if self._base_error is not None:
+            # self._base_error (SystemExit or KeyboardInterrupt) is about
+            # to propagate out of this method, which discards any other
+            # collected task errors silently.  Report them instead of
+            # losing them.  See gh-135736.
+            for suppressed_exc in self._errors:
+                self._loop.call_exception_handler({
+                    'message': 'TaskGroup task exception was not '
+                               'propagated because the TaskGroup body '
+                               'is being closed with a BaseException',
+                    'exception': suppressed_exc,
+                    'task_group': self,
+                })
             try:
                 raise self._base_error
             finally:
diff --git a/Lib/test/test_asyncio/test_taskgroups.py b/Lib/test/test_asyncio/test_taskgroups.py
index bc246400b83e9b2..f5b7daf9e2ce1d0 100644
--- a/Lib/test/test_asyncio/test_taskgroups.py
+++ b/Lib/test/test_asyncio/test_taskgroups.py
@@ -608,6 +608,39 @@ async def runner():
             get_error_types(cm.exception), {MyBaseExc, ZeroDivisionError}
         )
 
+    async def test_taskgroup_20b(self):
+        # Same setup as test_taskgroup_20 (a KeyboardInterrupt from the
+        # "async with" body itself, alongside a sibling task's exception):
+        # raising self._base_error out of _aexit() discards self._errors
+        # silently. The sibling's exception can't be raised alongside
+        # the KeyboardInterrupt (only one exception can propagate), but
+        # it must be reported via the loop's exception handler instead
+        # of being discarded silently. See gh-135736.
+        async def crash_soon():
+            await asyncio.sleep(0.1)
+            1 / 0
+
+        async def nested():
+            try:
+                await asyncio.sleep(10)
+            finally:
+                raise KeyboardInterrupt
+
+        async def runner():
+            async with taskgroups.TaskGroup() as g:
+                g.create_task(crash_soon())
+                await nested()
+
+        contexts = []
+        loop = asyncio.get_running_loop()
+        loop.set_exception_handler(lambda loop, context: contexts.append(context))
+
+        with self.assertRaises(KeyboardInterrupt):
+            await runner()
+
+        self.assertEqual(len(contexts), 1)
+        self.assertIsInstance(contexts[0]['exception'], ZeroDivisionError)
+
     async def _test_taskgroup_21(self):
         # This test doesn't work as asyncio, currently, doesn't
         # correctly propagate KeyboardInterrupt (or SystemExit) --
diff --git a/Misc/NEWS.d/next/Library/2026-07-23-12-27-52.gh-issue-135736.hkuGvu.rst b/Misc/NEWS.d/next/Library/2026-07-23-12-27-52.gh-issue-135736.hkuGvu.rst
new file mode 100644
index 000000000000000..76a8b7105f75850
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-07-23-12-27-52.gh-issue-135736.hkuGvu.rst
@@ -0,0 +1,5 @@
+Fix :class:`asyncio.TaskGroup` silently discarding errors from sibling
+tasks whenever the ``async with`` block exits with a :exc:`SystemExit` or
+:exc:`KeyboardInterrupt`. These errors are now reported via
+:meth:`loop.call_exception_handler() <asyncio.loop.call_exception_handler>`
+instead of being lost.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.