diff --git a/Lib/asyncio/taskgroups.py b/Lib/asyncio/taskgroups.py index 1fdffcc81a8865..1d3d26a5a56f48 100644 --- a/Lib/asyncio/taskgroups.py +++ b/Lib/asyncio/taskgroups.py @@ -116,6 +116,7 @@ async def _aexit(self, et, exc): # can be cancelled multiple times if our parent task # is being cancelled repeatedly (or even once, when # our own cancellation is already in progress) + pending_cancellation_error = None while self._tasks: if self._on_completed_fut is None: self._on_completed_fut = self._loop.create_future() @@ -123,6 +124,7 @@ async def _aexit(self, et, exc): try: await self._on_completed_fut except exceptions.CancelledError as ex: + pending_cancellation_error = ex if not self._aborting: # Our parent task is being cancelled: # @@ -151,6 +153,9 @@ async def _aexit(self, et, exc): # If there are no pending cancellations left, # don't propagate CancelledError. propagate_cancellation_error = None + elif propagate_cancellation_error is None: + # gh-155433: the remaining cancellation is not ours, don't drop it + propagate_cancellation_error = pending_cancellation_error # Propagate CancelledError if there is one, except if there # are other errors -- those have priority. diff --git a/Lib/test/test_asyncio/test_taskgroups.py b/Lib/test/test_asyncio/test_taskgroups.py index b1bff6b476c5dd..12438f1b8eb55c 100644 --- a/Lib/test/test_asyncio/test_taskgroups.py +++ b/Lib/test/test_asyncio/test_taskgroups.py @@ -1165,6 +1165,34 @@ async def child(tg): task = tg.create_task(child(tg)) self.assertTrue(task.cancelled()) + async def test_taskgroup_cancel_keeps_outer_cancellation(self): + # gh-155433: any cancellation from outside the group must propagate. + cancelling = asyncio.Event() + release = asyncio.Event() + + async def child(): + try: + await asyncio.sleep(10) + finally: + # The group is cancelling: it has cancelled its parent task + # and is waiting for this task to finish. + cancelling.set() + await release.wait() + + async def body(): + async with asyncio.TaskGroup() as tg: + tg.create_task(child()) + await asyncio.sleep(0) + tg.cancel() + + task = asyncio.create_task(body()) + await cancelling.wait() + task.cancel('message') + release.set() + with self.assertRaises(asyncio.CancelledError) as cm: + await task + self.assertEqual('message', cm.exception.args[0]) + async def test_taskgroup_cancel_before_exception(self): async def raise_exc(parent_tg: asyncio.TaskGroup): parent_tg.cancel() diff --git a/Misc/NEWS.d/next/Library/2026-08-09-16-14-38.gh-issue-155433.KL7tHV.rst b/Misc/NEWS.d/next/Library/2026-08-09-16-14-38.gh-issue-155433.KL7tHV.rst new file mode 100644 index 00000000000000..dc8961e976abbc --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-09-16-14-38.gh-issue-155433.KL7tHV.rst @@ -0,0 +1,2 @@ +Fix :class:`asyncio.TaskGroup` losing outside cancellation after +``cancel()``.