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]