New GitHub issue #93122 from HFrost0:<br>
<hr>
<pre>
<!--
If you're new to Python and you're not sure whether what you're experiencing is a bug, the CPython issue tracker is not
the right place to seek help. Consider the following options instead:
- reading the Python tutorial: https://docs.python.org/3/tutorial/
- posting in the "Users" category on discuss.python.org: https://discuss.python.org/c/users/7
- emailing the Python-list mailing list: https://mail.python.org/mailman/listinfo/python-list
- searching our issue tracker (https://github.com/python/cpython/issues) to see if
your problem has already been reported
-->
**Bug report**
Here is a minimal example:
```python
import asyncio
e = KeyboardInterrupt # or SystemExit
async def main_task():
await asyncio.gather(
sub_task(),
)
async def sub_task():
raise e
if __name__ == '__main__':
try:
asyncio.run(main_task())
except e:
print(f'Handle {e}')
```
This code handles the Interrupt normally, which is what im expected.
```
Handle <class 'KeyboardInterrupt'>
Process finished with exit code 0
```
But when I add the `asyncio.sleep(0)` (can be replaced by other task, not important) into `main_task`'s `asyncio.gather`
```python
import asyncio
e = KeyboardInterrupt # or SystemExit
async def main_task():
await asyncio.gather(
sub_task(),
asyncio.sleep(0)
)
async def sub_task():
raise e
if __name__ == '__main__':
try:
asyncio.run(main_task())
except e:
print(f'Handle {e}')
```
There is an unexpected traceback print out which is really confusing 💦, this traceback indicates that there is
**another** KeyboardInterrupt raised.
<details>
<summary>Full traceback</summary>
```
Traceback (most recent call last):
File "/Users/huanghuiling/PycharmProjects/Lighting-bilibili-download/tests/log_test.py", line 45, in <module>
asyncio.run(main_task())
File "/opt/homebrew/Caskroom/miniforge/base/envs/test9/lib/python3.9/asyncio/runners.py", line 47, in run
_cancel_all_tasks(loop)
File "/opt/homebrew/Caskroom/miniforge/base/envs/test9/lib/python3.9/asyncio/runners.py", line 63, in _cancel_all_tasks
loop.run_until_complete(
File "/opt/homebrew/Caskroom/miniforge/base/envs/test9/lib/python3.9/asyncio/base_events.py", line 629, in run_until_complete
self.run_forever()
File "/opt/homebrew/Caskroom/miniforge/base/envs/test9/lib/python3.9/asyncio/base_events.py", line 596, in run_forever
self._run_once()
File "/opt/homebrew/Caskroom/miniforge/base/envs/test9/lib/python3.9/asyncio/base_events.py", line 1890, in _run_once
handle._run()
File "/opt/homebrew/Caskroom/miniforge/base/envs/test9/lib/python3.9/asyncio/events.py", line 80, in _run
self._context.run(self._callback, *self._args)
File "/Users/huanghuiling/PycharmProjects/Lighting-bilibili-download/tests/log_test.py", line 7, in main_task
await asyncio.gather(
File "/opt/homebrew/Caskroom/miniforge/base/envs/test9/lib/python3.9/asyncio/runners.py", line 44, in run
return loop.run_until_complete(main)
File "/opt/homebrew/Caskroom/miniforge/base/envs/test9/lib/python3.9/asyncio/base_events.py", line 629, in run_until_complete
self.run_forever()
File "/opt/homebrew/Caskroom/miniforge/base/envs/test9/lib/python3.9/asyncio/base_events.py", line 596, in run_forever
self._run_once()
File "/opt/homebrew/Caskroom/miniforge/base/envs/test9/lib/python3.9/asyncio/base_events.py", line 1890, in _run_once
handle._run()
File "/opt/homebrew/Caskroom/miniforge/base/envs/test9/lib/python3.9/asyncio/events.py", line 80, in _run
self._context.run(self._callback, *self._args)
File "/Users/huanghuiling/PycharmProjects/Lighting-bilibili-download/tests/log_test.py", line 14, in sub_task
raise e
KeyboardInterrupt
Process finished with exit code 0
```
</details>
So I go deeply into the `asyncio.run`, and write code (a simplified `asyncio.run`) below to figure out what happen.
```python
import asyncio
e = KeyboardInterrupt # or SystemExit
async def main_task():
await asyncio.gather(
sub_task(),
asyncio.sleep(0)
)
async def sub_task():
raise e
if __name__ == '__main__':
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main_task())
except e:
print(f'Expected {e}')
finally:
try:
tasks = asyncio.all_tasks(loop)
for t in tasks:
t.cancel()
# ⬇️ this line will raise another KeyboardInterrupt which is unexpected ⬇️
loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
except e:
print(f'Unexpected {e} !!!!')
```
This line will raise another KeyboardInterrupt which is unexpected.
```python
loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
```
```
Expected <class 'KeyboardInterrupt'>
Unexpected <class 'KeyboardInterrupt'> !!!!
Process finished with exit code 0
```
Note that this line is used to cancel all tasks during gracefully shutdown (also in `asyncio.run`), and when I change
`e` to other error like `IndexError`(any `BaseException`), this code works fine without unexpected another exception.
I believe this is related to the `asyncio` treats `SystemExit` and `KeyboardInterrupt` in different way. For example
in `events.py`
```python
def _run(self):
try:
self._context.run(self._callback, *self._args)
except (SystemExit, KeyboardInterrupt):
raise
except BaseException as exc:
cb = format_helpers._format_callback_source(
self._callback, self._args)
msg = f'Exception in callback {cb}'
context = {
'message': msg,
'exception': exc,
'handle': self,
}
if self._source_traceback:
context['source_traceback'] = self._source_traceback
self._loop.call_exception_handler(context)
self = None # Needed to break cycles when an exception occurs.
```
My question is:
1. Why `asyncio.gather` behaves inconsistently.
2. Is there any reason to treat `KeyboardInterrupt` differently, since the simplest way
to solve this bug is to handle it same as `BaseException`.
I think user would like to handle all error consistently during the running of a task whether
it's `KeyboardInterrupt` or `BaseException`.
Even `asyncio` treats them in different way (incase really necessary)
```
asyncio.gather(sub_task())
```
and
```
asyncio.gather(sub_task(), asyncio.sleep(0))
```
should behave consistently, so I think this is a bug in asyncio.
> I think user would like to handle all error consistently during the running of a task whether
> it's `KeyboardInterrupt` or `BaseException`.
because in most case, user use `asyncio` to speed up network IO, the `KeyboardInterrupt` can be raised inside a task(
assume task is running), but also can be raised outside any task **when no task is running** (all of them waiting for IO
response), in this case `KeyboardInterrupt` raise inside event loop without any task exception handle.
```python
try:
asyncio.run(main_task())
except KeyboardInterrupt:
pass
```
should be works consistently in both case, but it works fine when all task is pending, while raise another unexpected
exception when a task is running
**Your environment**
<!-- Include as many relevant details as possible about the environment you experienced the bug in -->
- CPython versions tested on: 3.8, 3.9, 3.10
- Operating system and architecture: both macOS and windows
<!--
You can freely edit this text. Remove any lines you believe are unnecessary.
-->
</pre>
<hr>
<a href="https://github.com/python/cpython/issues/93122">View on GitHub</a>
<p>Labels: type-bug</p>
<p>Assignee: </p>