'Python asyncio program does not exit waiting for stdin input

I have a complex Python 3.9 program that does not exit if interrupted with Ctrl-C (SIGINT). The following is a minimal repro of the problem. Run with python sample.py You should see two prompts for input, then a normal exit. If you hit Ctrl-C on the first prompt, it exits as expected. If you hit Ctrl-C on the second prompt, nothing happens, until you hit Enter (and then an uncaught KeyboardInterrupt happens deep inside selectors.py).

import asyncio

async def ainput(prompt: str = "") -> str:
    return await asyncio.get_event_loop().run_in_executor(None, input, prompt)

async def main():
    try:
        input("1>")             # ctrl-C here works as expected, prints the message and exits
        await ainput("2>")      # ctrl-C here does nothing until you hit Enter
    except KeyboardInterrupt:
        print("Ctrl-C")

asyncio.run(main())

Question

Is this a bug in the code above? Or a bug in asyncio? How can I fix this?

Output

$ python sample.py
1>asks
2>lsls
$ python sample.py
1>^CCtrl-C
$ python sample.py
1>slsl
2>^C # **nothing happens here until you hit Enter**
Traceback (most recent call last):
  File "/Users/me/work/experimental/sample.py", line 15, in <module>
    asyncio.run(main())
  File "/opt/homebrew/Caskroom/miniforge/base/envs/alexa/lib/python3.9/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/opt/homebrew/Caskroom/miniforge/base/envs/alexa/lib/python3.9/asyncio/base_events.py", line 634, in run_until_complete
    self.run_forever()
  File "/opt/homebrew/Caskroom/miniforge/base/envs/alexa/lib/python3.9/asyncio/base_events.py", line 601, in run_forever
    self._run_once()
  File "/opt/homebrew/Caskroom/miniforge/base/envs/alexa/lib/python3.9/asyncio/base_events.py", line 1869, in _run_once
    event_list = self._selector.select(timeout)
  File "/opt/homebrew/Caskroom/miniforge/base/envs/alexa/lib/python3.9/selectors.py", line 562, in select
    kev_list = self._selector.control(None, max_ev, timeout)
KeyboardInterrupt


Solution 1:[1]

What happens is the KeyboardInterrupt is caught by asyncio since you are using threading. What asyncio then does is feed this into your coroutine by attempting to cancel it, which raises asyncio.CancelledError. You can handle it like this:

async def main():
    try:
        input("1>")
        await ainput("2>")
    except KeyboardInterrupt:
        print("Ctrl-C")
    except asyncio.CancelledError:
        print("Cancelled?")
        raise

There isn't any way to stop asyncio.run(...) from raising the KeyboardInterrupt however, at least AFAIK.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Simply Beautiful Art