Repository navigation
asyncio.gather with a single task raises an unexpected CancelledError exception when SystemExit occurs in this task. #108549
Description
Checklist
- I am confident this is a bug in CPython, not a bug in a third-party project
- I have searched the CPython issue tracker,
and am confident this bug has not been reported before
CPython versions tested on:
3.8, 3.9, 3.10, 3.11, 3.12
Operating systems tested on:
Linux, macOS
Description:
Occasionaly since last summer, I am working on this issue #93122 to figure out why behaviors are different in asyncio.gather when there is at least one task passed to this function, and when one of them, raises an SystemExit or KeybordInterrupt.
And I found odd behavior when there is only one task that raises SystemExit (or KeybordInterrupt), as below:
import asyncio
async def sub_task():
raise SystemExit
async def asyncio_gather():
try:
await asyncio.gather(
sub_task(),
)
except BaseException as ee:
print(f'{ee = }, id={hex(id(ee))}')
raise ee
if __name__ == '__main__':
try:
asyncio.run(asyncio_gather())
except BaseException as ee:
print(f'END {ee = }, id={hex(id(ee))}')Result is:
ee = CancelledError(), id=0x102c01e40
END ee = SystemExit(), id=0x102c01d80Raising CancelledError exception in asyncio_gather coroutine is really unexpected. IMO it should be SystemExit. Just to check, I created a new example with a TaskGroup where the same sub_task()coroutine is created in.
import asyncio
async def sub_task():
raise SystemExit
async def asyncio_taskgroup():
try:
async with asyncio.TaskGroup() as tg:
tg.create_task(sub_task())
except BaseException as ee:
print(f'{ee = }, id={hex(id(ee))}')
raise ee
if __name__ == '__main__':
try:
asyncio.run(asyncio_taskgroup())
except BaseException as ee:
print(f'END {ee = }, id={hex(id(ee))}') Result is:
ee = SystemExit(), id=0x103174520
END ee = SystemExit(), id=0x103174520
Task exception was never retrieved
future: <Task finished name='Task-1' coro=<asyncio_taskgroup() done, defined at /Users/yves/Desktop/Bugs/test_ghxxxxx.py:31> exception=SystemExit()>
Traceback (most recent call last):
...
...And to complete the problem analysis, from the first example, when asyncio.gather is called with 2 tasks as await asyncio.gather(sub_task(), asyncio.sleep(0.5)), SystemExit is raised, not CancelledError. It seems really inconsistent to raise a CancelledError exception.
Analysis:
I tried to understand why CancelledError is raised in the first example.
When the sub_task coroutine raises systemExit, this exception is propagated up to the RunnerContext Manager created in asyncio.run and finally treated in the __exit__ method of this CM.
Through the Runner.close method, there is a call to cancel_all_tasks. In this function, only one task is pending and candidate to cancel: asyncio_gather. The sub_task coroutine which raised SystemExit is finsihed. This part is processing in the call to asyncio.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True)).
At this point, asyncio_gather task waits for its GatheringFuture future ending. Later in the processing of event loop, two callbacks are executed:
gather.<locals>._done_callbackon the pendingGatheringFuturefuture, setting its attribut_exceptiontoSystemExitas result ofsub_taskcoroutine.Task.__wakeuponasyncio_gatherpending task, where a call tofuture.result()cause anSystemExitexception that is going to pass as argument toself.__step.
In this _step method, exc is changed to CancelledError, and passed to self.__step_run_and_handle_result(exc). That triggers the CancelledError exception.
Question:
Is this really an issue ? Could a cancelling task be raised CancelledError when its _exception attribute is already set to SystemExit or KeybordInterrupt ?
Maybe we need to discuss this before validating the problem and finding a fix?
Possible fix (if this is really an issue):
In case of issue/feature, a fix is to change the behavior of Task.__step as follow:
when a task is 'to cancel', the exc variable could be overwrite to CancelledError, only if exc is not already an instance of CancelledError, SystemExit or KeybordInterrupt, as below:
index 8d5bde09ea..07f916dc6d 100644
--- a/Lib/asyncio/tasks.py
+++ b/Lib/asyncio/tasks.py
@@ -283,7 +283,12 @@ 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,
+ SystemExit, KeyboardInterrupt
+ )):
exc = self._make_cancelled_error()
self._must_cancel = False
self._fut_waiter = Noneif we validate this feature, the fix should also be made to the asyncio module: _asynciomodule.c to validate test.test_asyncio.
Linked PRs
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsDone
Activity
Hels15 commentedon Aug 31, 2023
Well, I don't know much about exception handling in asyncio, but this solution sounds interesting, As you may know, the
__stepmethod is low-level and used to do one iteration which the event loop can use, also using generator terminology. We are not sure if this is an issue but this might not be the best solution, or at least looks strange to me.Hels15 commentedon Sep 1, 2023
@YvesDup not sure what you have in mind, Your research definitely valuable might as well ask about it here(under async tag): https://discuss.python.org/.
YvesDup commentedon Sep 4, 2023
I think this a bug, what is your opinion ?
Have you any suggestion ?
gvanrossum commentedon Sep 4, 2023
@YvesDup You're probably better off ignoring Hels15 -- even though they seem interested in learning, it seems like they are out of their depth here. I (or @kumaraditya303 or @1st1 or another asyncio expert) will get back to you for this issue, but since it's old and complicated it may be a while.
In the meantime you may want to just treat this as a feature: asyncio treats
SystemExit(andKeyboardInterrupt) different from otherBaseExceptionsubclasses, and this is deeply ingrained in the code base, for reasons of backwards compatibility and user expectation. It might be better if you could avoid raising those exceptions in your application (but I understand this may be beyond your control).YvesDup commentedon Aug 1, 2026
@kumaraditya303 could you please take a look at this old issue. If you feel this direct request is inappropriate, please accept my apologies for bothering you.
kumaraditya303 commentedon Aug 3, 2026
I'll try to take a look soon.
kumaraditya303 commentedon Aug 14, 2026
I think your provided fix is correct, can you create a PR with tests?
YvesDup commentedon Aug 15, 2026
Thank for your feedback @kumaraditya303, I will create a PR soon.
gh-108549: fix asyncio.Task cancellation swallowing SystemExit and Ke…
pythongh-108549: fix asyncio.Task cancellation swallowing SystemExit …
pythongh-108549: fix asyncio.Task cancellation swallowing SystemExit …