Repository navigation
Removal of implicit event loop creation in asyncio.get_event_loop() breaks batteries-included promise #149160
Description
Removal of implicit event loop creation in asyncio.get_event_loop() breaks batteries-included usage
Summary
Starting in Python 3.10, asyncio.get_event_loop() began emitting a DeprecationWarning when called without a running loop. As of Python 3.14, the default policy's get_event_loop() raises RuntimeError in that case (gh-93453), and the entire policy system is scheduled for removal in 3.16 (gh-127949).
This breaks a usage pattern that was valid, documented, and recommended for roughly a decade. It also breaks the "batteries included" expectation that synchronous entry points can obtain a loop, schedule work on it, and run it — without the caller itself being a coroutine.
What used to work
import asyncio
async def hello():
return "hi"
loop = asyncio.get_event_loop()
result = loop.run_until_complete(hello())On 3.14 this raises:
RuntimeError: There is no current event loop in thread 'MainThread'.
This pattern appears in pre-3.10 official asyncio documentation, in published asyncio books, in Jupyter/IPython integration, and in countless library-internal helpers that need a loop reference from synchronous code without owning the loop's lifecycle.
Why "just use asyncio.run()" is not a complete answer
The standard reply to this complaint is "use asyncio.run(hello())." That is fine for self-contained scripts, but it does not cover the cases the old API did:
- Library code that needs a loop reference without owning the loop's lifecycle. A library function called from synchronous user code may need to schedule a coroutine onto whatever loop is appropriate.
asyncio.run()creates and tears down a loop, which is the wrong semantics.asyncio.get_running_loop()only works inside a coroutine. - REPL and notebook workflows where users want a persistent loop across cells.
asyncio.run()constructs and discards a loop on each call. - Multi-coroutine sync entry points that run several coroutines on the same loop in sequence and inspect state between calls.
- Existing code in the wild. Years of correctly-written asyncio code follows the previous pattern. It was not wrong code — it was code written against the documented API.
asyncio.Runner (3.11+) addresses some of case 3 but is not a drop-in replacement and does not solve case 1.
Why this matters
"Batteries included" means installing Python gets you a stable, documented standard library. Breaking changes to widely-used standard library APIs — especially ones that were the recommended pattern — impose real ecosystem cost:
- Tutorials, books, and existing answers across the internet silently become wrong.
- Downstream libraries have to gate behavior on Python version, which churns transitive dependencies.
- Newcomers following any pre-3.10 material hit a
RuntimeErrorwith no context to debug it. - The migration replaces one well-understood call with conditional logic across
run/Runner/get_running_loop, depending on context.
The deprecation rationale in gh-127949 — "Loops are always per thread, there is no need to have a 'current loop' when no loop is currently running" — is a design preference, not a bug fix. The cost of acting on that preference is borne by every downstream user who wrote code against the API as it was documented.
Request
One of:
- Restore implicit-create behavior for
get_event_loop()(or expose an equivalentasyncio.ensure_event_loop()) as a stable, non-deprecated API. The implementation is well-understood; what is being removed is convenience, not correctness. - Provide an explicit replacement for the sync-entry-point case that does not require the caller to own the loop lifecycle.
asyncio.run()andRunnerdo not cover this. - At minimum, publish a complete migration matrix that maps each prior use of
get_event_loop()(sync entry point, library helper, REPL, multi-coro sync, third-party integration) to its 3.14+ equivalent. The current guidance reduces to "useasyncio.run()," which is incomplete and is the source of the breakage reports being filed against downstream projects.
References
- Finish deprecation in asyncio.get_event_loop() #93453 — Finish deprecation in
asyncio.get_event_loop() - Deprecate asyncio policy system #127949 — Deprecate asyncio policy system
- asyncio: Re-reverse deprecation of set_event_loop? #130322 —
asyncio: Re-reverse deprecation ofset_event_loop? - 3.14 docs: https://docs.python.org/3/library/asyncio-eventloop.html
CPython versions tested on:
3.10
Operating systems tested on:
Other, Linux
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsDone
- StatusShow more project fieldsTodo
Activity
[-]Removal of implicit event loop creation in `asyncio.get_event_loop()` breaks batteries-included usage[/-][+]Removal of implicit event loop creation in `asyncio.get_event_loop()` breaks batteries-included promise[/+]bdarnell commentedon Apr 29, 2026
As the maintainer of Tornado, I was annoyed by these changes when they began in 3.10, for the same reasons you give here. Tornado supported implicit event loop creation from the beginning, and this was the preferred pattern, so these changes to asyncio forced Tornado to break compatibility with over a decade of existing usage.
However, I became convinced that this was the right thing to do for asyncio, because
asyncio.run()is implemented differently than Tornado's nearly equivalentIOLoop.run_sync(), in a way that is error-prone. The difference is thatasyncio.run()doesn't grab the implicit event loop and run it, it explicitly creates a new one and runs that, so that code relying on implicit event loop creation viaget_event_loop()cannot coexist with code that usesasyncio.run(). Therefore, there were two viable paths forward: deprecate and remove the implicit creation of the event loop inget_event_loop(), or abandonasyncio.run()(or change its implementation) so that everything used implicit loop creation. I preferred the former, but I support the asyncio developer's choice to standardize onasyncio.run(), especially with a longer-than-usual deprecation timeline.I was tagged into this conversation via #130322, which was about a separate matter regarding
asyncio.set_event_loop(which is now restored). This also gives you a way forward for legacy code that does not useasyncio.run(): If you callasyncio.set_event_loop(asyncio.new_event_loop())at the top of your program, you can continue to useget_event_loopas before, provided you also start the event loop withget_event_loop().run_forever()and do not useasyncio.run().SoundsSerious commentedon Apr 29, 2026
@bdarnell Thanks for that, your comment is really insightful especially since I am not in the core development space. Tagging @glyph since he may find this interesting.
This part was especially helpful, and I think reveals the path forward
Thinking forward -- What would be wrong with creating a
LoopSingleton? this would be created at the first invocation of any async code and stays alive for the rest of the process, unlessLoopSingletonis explicitly shutdown. Unless you're running an events the overhead should be near zero, I am under the impression that all async code should be running in the main thread anyways.asyncio.runseems like it should be more of acreate_task + run_until_completeoriented workflow or am I missing something. Its a convenient way to run and wait on async code. If an application wants to keep theasyncio.runevent-shutdown behavior that function can get ado_shutdownargument for backwards compatibility.picnixz commentedon Apr 29, 2026
cc @kumaraditya303
I am removing the bug label because I think this was already discussed but feel free to add it back Kumar.
kumaraditya303 commentedon May 12, 2026
These APIs have been deprecated from a long time ago and there is a clear migration path forward, I kept some "hacks" like
asyncio.set_event_loopto ease migration which you can still use. Otherwise this doesn't seem actionable to me so I'll close this.SoundsSerious commentedon May 12, 2026
@kumaraditya303 @picnixz @tiran Closing as "not actionable" doesn't address the substance of the case, so I'd like to put it more sharply, with citations.
PEP 3156, the foundational asyncio design document, names interoperability between frameworks as a primary design goal and identifies the policy system —
get_event_loop()/set_event_loop()— as one of two APIs specified to support that goal:The same PEP documents lazy event-loop creation as the by-design behavior of the default policy, not as a transitional convenience:
And it explicitly contemplates third-party library code calling
get_event_loop():The 3.10–3.14 deprecation removes this interoperability primitive. The replacement APIs —
asyncio.run()andget_running_loop()— cover entry-point lifecycle and intra-coroutine loop access. Neither covers the cross-framework integration case PEP 3156 explicitly named and prescribedget_event_loop()for.The "clear migration path" framing therefore isn't accurate for the case at issue.
set_event_loop(new_event_loop())works as a workaround, and I appreciate that you kept it, but characterizing it as a "hack to ease migration" is the opposite of what it should be: it's currently the only sanctioned way to satisfy PEP 3156's interoperability requirement, and downstream libraries need to know whether they can build on it.The actionable ask remains the same: either document
set_event_loop(new_event_loop())as a supported, non-deprecated pattern for the library-bootstrap case, or add an explicit primitive that serves that case. Without one of those, the deprecation has left a hole where a documented design pillar used to be.I'd appreciate a substantive response to the PEP citation rather than a re-close. If the asyncio team's current position is that PEP 3156's interoperability section is superseded, saying so would itself be useful — it would tell downstream library authors what to expect.
kumaraditya303 commentedon May 13, 2026
PEPs are historical documents and do not reflect the current status.
set_event_loopis not deprecated and is part of the public API, it is not going anywhere and is supported.