diff --git a/Lib/asyncio/runners.py b/Lib/asyncio/runners.py index ba37e003a655c0..f864ed8c452788 100644 --- a/Lib/asyncio/runners.py +++ b/Lib/asyncio/runners.py @@ -158,12 +158,14 @@ def _lazy_init(self): def _on_sigint(self, signum, frame, main_task): self._interrupt_count += 1 - if self._interrupt_count == 1 and not main_task.done(): + + if not main_task.done(): main_task.cancel() - # wakeup loop if it is blocked by select() with long timeout self._loop.call_soon_threadsafe(lambda: None) - return - raise KeyboardInterrupt() + + + if self._interrupt_count > 10: + raise KeyboardInterrupt() def run(main, *, debug=None, loop_factory=None): diff --git a/Lib/test/test_asyncio/test_runners.py b/Lib/test/test_asyncio/test_runners.py index 8a4d7f5c796661..32058ba30fc630 100644 --- a/Lib/test/test_asyncio/test_runners.py +++ b/Lib/test/test_asyncio/test_runners.py @@ -522,6 +522,51 @@ async def coro(): self.assertEqual(0, result.repr_count) + def test_nested_keyboardinterrupt_handling(self): + """Test that multiple KeyboardInterrupts are handled correctly.""" + results = [] + + async def nested_coro(): + try: + while True: + await asyncio.sleep(0.1) + results.append('*') + except asyncio.CancelledError: + results.append('first_cancelled') + try: + while True: + await asyncio.sleep(0.1) + results.append('#') + except asyncio.CancelledError: + results.append('second_cancelled') + try: + while True: + await asyncio.sleep(0.1) + results.append('!') + except asyncio.CancelledError: + results.append('third_cancelled') + + + def run_with_cancels(): + async def main(): + task = asyncio.create_task(nested_coro()) + await asyncio.sleep(0.2) + task.cancel() + await asyncio.sleep(0.2) + task.cancel() + await asyncio.sleep(0.2) + task.cancel() + await asyncio.sleep(0.1) + + asyncio.run(main()) + + run_with_cancels() + + + self.assertIn('first_cancelled', results) + self.assertIn('second_cancelled', results) + self.assertIn('third_cancelled', results) + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-03-20-06-26-14.gh-issue-146194.aXjDnd.rst b/Misc/NEWS.d/next/Library/2026-03-20-06-26-14.gh-issue-146194.aXjDnd.rst new file mode 100644 index 00000000000000..9951e0d8dcd63a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-20-06-26-14.gh-issue-146194.aXjDnd.rst @@ -0,0 +1,4 @@ +Fix nested :exc:`KeyboardInterrupt` handling in :mod:`asyncio`. Previously, +multiple Ctrl+C presses would cause a crash with ``Task was destroyed but it +is pending!``. Now nested cancellations propagate correctly through multiple +levels.