From 7500655a6883d4eb448ab1b727a188d5e00e3468 Mon Sep 17 00:00:00 2001 From: "Yves.Duprat" Date: Fri, 21 Aug 2026 00:21:01 +0200 Subject: [PATCH 1/7] Initial commit - Update `Task.__step` , - Update `_asyncio` module on the corresponding function, - Add a `test_step_dont_swallow_systemexit` test in `test_task.py` file. --- Lib/asyncio/tasks.py | 6 +++++- Lib/test/test_asyncio/test_tasks.py | 19 +++++++++++++++++++ Modules/_asynciomodule.c | 9 +++++++-- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 7889d4793a5dec3..524be6224dc1552 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -268,7 +268,11 @@ def __step(self, exc=None): raise exceptions.InvalidStateError( f'__step(): already done: {self!r}, {exc!r}') if self._must_cancel: - if not isinstance(exc, exceptions.CancelledError): + if not isinstance(exc, (exceptions.CancelledError, + # gh-108549: do not swallow these + # 2 exceptions + SystemExit, KeyboardInterrupt) + ): exc = self._make_cancelled_error() self._must_cancel = False self._fut_waiter = None diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index 9c111da8c27f162..8d11238fbd4449a 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -1891,6 +1891,25 @@ async def notmuch(): self.loop.run_until_complete(task), 'ko') + def test_step_dont_swallow_systemexit(self): + # gh-108549: do not swallow + # KeybordInterrupt too. + async def sub_task(): + raise SystemExit + + async def gather(): + try: + await asyncio.gather(sub_task(),) + except SystemExit: + pass + + t = self.new_task(self.loop, gather()) + with self.assertRaises(SystemExit): + self.loop.run_until_complete(t) + t.cancel() + self.loop.run_until_complete(t) + self.assertTrue(t.done()) + def test_step_result_future(self): # If coroutine returns future, task waits on this future. diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 41384b388142ccc..03ea94a6c3ded36 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -3067,8 +3067,13 @@ task_step_impl(asyncio_state *state, TaskObj *task, PyObject *exc) if (task->task_must_cancel) { assert(exc != Py_None); - if (!exc || !PyErr_GivenExceptionMatches(exc, state->asyncio_CancelledError)) { - /* exc was not a CancelledError */ + if (!exc || + (!PyErr_GivenExceptionMatches(exc, state->asyncio_CancelledError) && + !PyErr_GivenExceptionMatches(exc, PyExc_KeyboardInterrupt) && + !PyErr_GivenExceptionMatches(exc, PyExc_SystemExit)) + ) { + /* exc was not a CancelledError, + neither SystemExit or KeyboardInterrupt (see gh-108549 )*/ exc = create_cancelled_error(state, (FutureObj*)task); if (!exc) { From d66b44f80a18a41a88d48438b714bac1ed1c46ff Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 14:28:56 +0000 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst diff --git a/Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst b/Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst new file mode 100644 index 000000000000000..8d18a14d33a5e40 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst @@ -0,0 +1 @@ +In ``asyncio.Task.__step``, do not cancel the current task when a ``SystemExit``(or a ``KeyboardInterrupt``) exception was just raised. From a89d0d5c02edee7770359fab43fcf5df43284262 Mon Sep 17 00:00:00 2001 From: "Yves.Duprat" Date: Mon, 24 Aug 2026 16:42:30 +0200 Subject: [PATCH 3/7] Fix nits in news file. --- .../next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst b/Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst index 8d18a14d33a5e40..c97425a91921f54 100644 --- a/Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst +++ b/Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst @@ -1 +1 @@ -In ``asyncio.Task.__step``, do not cancel the current task when a ``SystemExit``(or a ``KeyboardInterrupt``) exception was just raised. +In ``asyncio.Task.__step``, do not cancel the current task when a ``SystemExit`` (or a ``KeyboardInterrupt``) exception was just raised. From bb5ec1586b086f9e8e1d560b59ada38174d2ec8a Mon Sep 17 00:00:00 2001 From: "Yves.Duprat" Date: Thu, 27 Aug 2026 12:31:59 +0200 Subject: [PATCH 4/7] Format short comments on a single line --- Lib/asyncio/tasks.py | 3 +-- Lib/test/test_asyncio/test_tasks.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 1c63472aae25a6e..db4e5a7eda60110 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -269,8 +269,7 @@ def __step(self, exc=None): f'__step(): already done: {self!r}, {exc!r}') if self._must_cancel: if not isinstance(exc, (exceptions.CancelledError, - # gh-108549: do not swallow these - # 2 exceptions + # gh-108549: do not swallow these exceptions SystemExit, KeyboardInterrupt) ): exc = self._make_cancelled_error() diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index 8d11238fbd4449a..84aba2db4e7d0cb 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -1892,8 +1892,7 @@ async def notmuch(): 'ko') def test_step_dont_swallow_systemexit(self): - # gh-108549: do not swallow - # KeybordInterrupt too. + # gh-108549: do not swallow KeybordInterrupt too. async def sub_task(): raise SystemExit From 5ae44ea5905f652f5f54d7fbb20511c284fb1867 Mon Sep 17 00:00:00 2001 From: "Yves.Duprat" Date: Fri, 28 Aug 2026 22:51:03 +0200 Subject: [PATCH 5/7] Apply remarks and suggestions from @kumaraditya303 --- Lib/asyncio/tasks.py | 2 +- Lib/test/test_asyncio/test_tasks.py | 29 ++++++++++++++++------------- Modules/_asynciomodule.c | 7 +++---- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index db4e5a7eda60110..c613d8e2228279b 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -268,8 +268,8 @@ def __step(self, exc=None): raise exceptions.InvalidStateError( f'__step(): already done: {self!r}, {exc!r}') if self._must_cancel: + # gh-108549: do not swallow SystemExit and KeyboardInterrupt if not isinstance(exc, (exceptions.CancelledError, - # gh-108549: do not swallow these exceptions SystemExit, KeyboardInterrupt) ): exc = self._make_cancelled_error() diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index 84aba2db4e7d0cb..45c752ff6c50c43 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -1891,23 +1891,26 @@ async def notmuch(): self.loop.run_until_complete(task), 'ko') - def test_step_dont_swallow_systemexit(self): - # gh-108549: do not swallow KeybordInterrupt too. - async def sub_task(): - raise SystemExit + def test_step_dont_swallow_systemexit_or_keyboardinterrupt(self): + # see gh-108549: do not swallow SystemExit and KeyboardInterrupt + # in Task.__step when the current task must be cancelled. + async def sub_task(exc): + raise exc - async def gather(): + async def current_task(exc): try: - await asyncio.gather(sub_task(),) - except SystemExit: + await asyncio.create_task(sub_task(exc)) + except exc: pass + except BaseException as e: + self.fail(f'{exc} is expected, instead of {type(e)}') - t = self.new_task(self.loop, gather()) - with self.assertRaises(SystemExit): - self.loop.run_until_complete(t) - t.cancel() - self.loop.run_until_complete(t) - self.assertTrue(t.done()) + for exc in (SystemExit, KeyboardInterrupt): + t = self.new_task(self.loop, current_task(exc)) + self.assertRaises(exc, self.loop.run_until_complete, t) + t.cancel() + test_utils.run_briefly(self.loop) + self.assertTrue(t.done()) def test_step_result_future(self): # If coroutine returns future, task waits on this future. diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 6d808eb71f3c361..979ecb6735b2871 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -3051,13 +3051,12 @@ task_step_impl(asyncio_state *state, TaskObj *task, PyObject *exc) if (task->task_must_cancel) { assert(exc != Py_None); + /* exc was not a CancelledError, neither SystemExit or KeyboardInterrupt. + See gh-108549 for explanation about the last two exceptions */ if (!exc || (!PyErr_GivenExceptionMatches(exc, state->asyncio_CancelledError) && !PyErr_GivenExceptionMatches(exc, PyExc_KeyboardInterrupt) && - !PyErr_GivenExceptionMatches(exc, PyExc_SystemExit)) - ) { - /* exc was not a CancelledError, - neither SystemExit or KeyboardInterrupt (see gh-108549 )*/ + !PyErr_GivenExceptionMatches(exc, PyExc_SystemExit))) { exc = create_cancelled_error(state, (FutureObj*)task); if (!exc) { From d9d7766b530b080b70e35a3c693f66a7117af943 Mon Sep 17 00:00:00 2001 From: "Yves.Duprat" Date: Sat, 29 Aug 2026 12:22:03 +0200 Subject: [PATCH 6/7] Apply last changes from review --- Lib/test/test_asyncio/test_tasks.py | 11 ++++++----- .../2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst | 4 +++- Modules/_asynciomodule.c | 5 +++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index 45c752ff6c50c43..cc747c80c86723e 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -1906,11 +1906,12 @@ async def current_task(exc): self.fail(f'{exc} is expected, instead of {type(e)}') for exc in (SystemExit, KeyboardInterrupt): - t = self.new_task(self.loop, current_task(exc)) - self.assertRaises(exc, self.loop.run_until_complete, t) - t.cancel() - test_utils.run_briefly(self.loop) - self.assertTrue(t.done()) + with self.subTest(exc): + t = self.new_task(self.loop, current_task(exc)) + self.assertRaises(exc, self.loop.run_until_complete, t) + t.cancel() + test_utils.run_briefly(self.loop) + self.assertTrue(not t.cancelled()) def test_step_result_future(self): # If coroutine returns future, task waits on this future. diff --git a/Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst b/Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst index c97425a91921f54..6b0bf82b65257c2 100644 --- a/Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst +++ b/Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst @@ -1 +1,3 @@ -In ``asyncio.Task.__step``, do not cancel the current task when a ``SystemExit`` (or a ``KeyboardInterrupt``) exception was just raised. +Fix :class:`asyncio.Task`, when the task has a pending cancellation, +replace **exc** exception with a :exc:`CancelledError` unless it is +:exc:`SystemExit` or :exc:`KeyboardInterrupt`, which must propagate unchanged diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 979ecb6735b2871..8c90b0b1517ae4e 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -3051,8 +3051,9 @@ task_step_impl(asyncio_state *state, TaskObj *task, PyObject *exc) if (task->task_must_cancel) { assert(exc != Py_None); - /* exc was not a CancelledError, neither SystemExit or KeyboardInterrupt. - See gh-108549 for explanation about the last two exceptions */ + /* Replace exc with a CancelledError unless it already is one, or + it is SystemExit/KeyboardInterrupt, which must propagate + unchanged (gh-108549). */ if (!exc || (!PyErr_GivenExceptionMatches(exc, state->asyncio_CancelledError) && !PyErr_GivenExceptionMatches(exc, PyExc_KeyboardInterrupt) && From d09b9f31f668f2f7d8d6bae4673182de02ded4e0 Mon Sep 17 00:00:00 2001 From: Duprat Date: Sat, 29 Aug 2026 13:02:28 +0200 Subject: [PATCH 7/7] Fix nit in news file. --- .../Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst b/Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst index 6b0bf82b65257c2..c6b7605f1b0eb56 100644 --- a/Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst +++ b/Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst @@ -1,3 +1,3 @@ Fix :class:`asyncio.Task`, when the task has a pending cancellation, -replace **exc** exception with a :exc:`CancelledError` unless it is -:exc:`SystemExit` or :exc:`KeyboardInterrupt`, which must propagate unchanged +replace **exc** exception with a :exc:`asyncio.CancelledError` unless it is +:exc:`SystemExit` or :exc:`KeyboardInterrupt`, which must propagate unchanged.