Repository navigation
asyncio.run(...) hangs and neither returns nor raises after asyncio.new_event_loop() called within #19718
Description
Port, board and/or hardware
RP2 port, RP2350
MicroPython version
MicroPython v1.29.0 on 2026-08-24; Raspberry Pi Pico 2 W with RP2350
Reproduction
- Copy paste the code provided below into a new file.
# Mark A. Ziesemer, 2026-09-19
import asyncio
async def task1():
for i in range(6):
print(f" + i: {i}")
await asyncio.sleep(1)
print(" - task1 complete.")
async def task2():
for j in range(3):
print(f" + j: {j}")
await asyncio.sleep(1)
# This should never happen, but here we are...
print("3. about to call asyncio.new_event_loop()...")
asyncio.new_event_loop()
print("4. after asyncio.new_event_loop().")
print(" - CPython: Continues on, and #4 will be seen.")
print(" - MicroPython: Will just hang.")
print(" - task2 complete.")
async def run():
t1 = asyncio.create_task(task1())
t2 = asyncio.create_task(task2())
print("2. tasks started.")
await asyncio.gather(t1, t2)
def main():
try:
print("1. before asyncio.run...")
asyncio.run(run())
# This line will never get hit (graceful exit with no exception)...
print("5. after asyncio.run...")
finally:
# This line will also never get hit...
print("6. after asyncio.run via finally.")
if __name__ == "__main__":
main()- Use
mpremote runto execute it on the board.
Expected behaviour
Expected to reach and output step # 6, as successfully observed when run with CPython:
1. before asyncio.run...
2. tasks started.
+ i: 0
+ j: 0
+ i: 1
+ j: 1
+ i: 2
+ j: 2
+ i: 3
3. about to call asyncio.new_event_loop()...
4. after asyncio.new_event_loop().
- CPython: Continues on, and #4 will be seen.
- MicroPython: Will just hang.
- task2 complete.
+ i: 4
+ i: 5
- task1 complete.
5. after asyncio.run...
6. after asyncio.run via finally.
Observed behaviour
After a call to asyncio.new_event_loop, running tasks are aborted, but asyncio.run(...) never returns - neither by exception nor by a graceful exit. The program then hangs indefinitely.
>>> import asyncio_test
>>> asyncio_test.main()
1. before asyncio.run...
2. tasks started.
+ i: 0
+ j: 0
+ i: 1
+ j: 1
+ i: 2
+ j: 2
+ i: 3
3. about to call asyncio.new_event_loop()...
4. after asyncio.new_event_loop().
- CPython: Continues on, and #4 will be seen.
- MicroPython: Will just hang.
- task2 complete.
(program hangs here)
Additional Information
This is observed when hooking aiorepl into a program, then pressing Ctrl+D during execution, as documented:
Ctrl-D at the asyncio REPL command prompt will terminate the current event loop, which will stop the running program and return to the regular REPL.
As seen at aiorepl.py#L190 in response to that action:
# Shutdown asyncio.
asyncio.new_event_loop()
returnReplacing task2() above with aiorepl.task() allows for reproducing exactly the same hang, though aiorepl does not directly run under CPython to easily show the comparison.
The same can be reproduced by running the main.py example from aiorepl#usage, and uncommenting the print("task 1") line. task aborts, but the program never completes. As such, the explanation that one will "return to the regular REPL" does not happen - at least not first without also then using Ctrl+C.
I don't believe this is an issue with aiorepl, but that asyncio.run(...) should either allow other existing tasks to run to completion and return (as it does in CPython), or at least raise an exception.
In all cases, Ctrl+C can and must then be used to raise a KeyboardInterrupt and finally quit the program. However, the issue is that the program itself has no way that I've found to even detect this and either recover or abort.
(For now, as what I think is an ugly workaround, I patch asyncio.new_event_loop to a method that calls for a graceful stop to my program.)
Related Issues
(or at least somewhat related)
- extmod/uasyncio: Add Loop.new_event_loop method. #5890
- I think this is the most relevant, and stems from cancellation error in extmod/uasyncio #5820.
- Incorrect uasyncio behaviour: tasks are not correctly terminated #7471
- uasyncio : missing RuntimeError: This event loop is already running #8515
- uasyncio: current_task behaviour differs from CPython #11530
- asyncio try/except/finally handling skipped if
asyncio.run()catches KeyboardInterrupt - asyncio.start_server() keeps running #15761
Code of Conduct
Yes, I agree
Activity
peterhinch commentedon Sep 20, 2026
I can confirm this.
However a word of caution may be in order. The usual application for issuing
new_event_loopin asynchronous code is to provide multiple threads with separate event loops. This use of threading is not supported in MicroPython.new_event_loopcreates new task and IO queues, leaving the current task's queues unreferenced and available for GC. Its main use is therefore for cleanup in synchronous code after the termination ofasyncio.run(). Arguablynew_event_loopshould throw if called from asynchronous code.ziesemer commentedon Sep 20, 2026
Agreed - which gets into some of your prior writings that I've been separately looking at, including:
I do think it would be great to support a 2nd even loop running on the 2nd core (using a 2nd task queue, and proper developer considerations for thread safety, etc.) - but I digress, and another topic for another effort.
I think the primary consideration here stems back to #5890:
Here it was decided to use
new_event_loop()not to create an additional event loop, but to "reset the singleton event loop". The above then continues with "to make it possible to write code that is compatible with CPython" - but with what we're seeing here, I do not think this goal was realized, and that this should be revisited?Agreed. At least until such time that maybe it could be used to run a 2nd event loop to run on the 2nd core? Though even then, this should not be called from within asynchronous code.
What to then do with
aioreplin this example, which seems to depend on callingnew_event_loop()to exit (and still really doesn't)? Update it to take in a function that can be called to instead invoke a graceful exit in the app that started it as a task, and then allow itself to simply return and complete its own task? Effectively formalizing what I'm already doing in my current code in patchingasyncio.new_event_loopto accomplish the same?Are there any potential legitimate usages of
new_event_loopalready being called from async that we would need to worry about breaking if this is changed to raise / throw an exception? (Though I can't immediately think of where it would be used legitimately, given what I've already covered above...)