Skip to content

asyncio.run(...) hangs and neither returns nor raises after asyncio.new_event_loop() called within #19718

Description

@ziesemer

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

  1. 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()
  1. Use mpremote run to 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()
return

Replacing 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)

Code of Conduct

Yes, I agree

Activity

  1. peterhinch commented on Sep 20, 2026

    @peterhinch
    Contributor

    I can confirm this.

    However a word of caution may be in order. The usual application for issuing new_event_loop in asynchronous code is to provide multiple threads with separate event loops. This use of threading is not supported in MicroPython.

    new_event_loop creates 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 of asyncio.run(). Arguably new_event_loop should throw if called from asynchronous code.

  2. ziesemer commented on Sep 20, 2026

    @ziesemer
    Author

    However a word of caution may be in order. The usual application for issuing new_event_loop in asynchronous code is to provide multiple threads with separate event loops. This is not supported in MicroPython, not least because the task queue is a singleton and is not thread safe.

    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:

    Following on from #5820, this PR adds Loop.new_event_loop() which is used to reset the singleton event loop. This functionality is put here instead of in Loop.close() to make it possible to write code that is compatible with CPython.

    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?

    Arguably new_event_loop should throw if called from asynchronous code.

    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 aiorepl in this example, which seems to depend on calling new_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 patching asyncio.new_event_loop to accomplish the same?

    Are there any potential legitimate usages of new_event_loop already 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...)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions