Skip to content

Removal of implicit event loop creation in asyncio.get_event_loop() breaks batteries-included promise #149160

Closed as not planned

Description

@SoundsSerious

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:

  1. 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.
  2. REPL and notebook workflows where users want a persistent loop across cells. asyncio.run() constructs and discards a loop on each call.
  3. Multi-coroutine sync entry points that run several coroutines on the same loop in sequence and inspect state between calls.
  4. 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 RuntimeError with 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:

  1. Restore implicit-create behavior for get_event_loop() (or expose an equivalent asyncio.ensure_event_loop()) as a stable, non-deprecated API. The implementation is well-understood; what is being removed is convenience, not correctness.
  2. Provide an explicit replacement for the sync-entry-point case that does not require the caller to own the loop lifecycle. asyncio.run() and Runner do not cover this.
  3. 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 "use asyncio.run()," which is incomplete and is the source of the breakage reports being filed against downstream projects.

References

CPython versions tested on:

3.10

Operating systems tested on:

Other, Linux

Activity

  1. changed the title [-]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[/+] on Apr 29, 2026
  2. bdarnell commented on Apr 29, 2026

    @bdarnell
    Contributor

    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 equivalent IOLoop.run_sync(), in a way that is error-prone. The difference is that asyncio.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 via get_event_loop() cannot coexist with code that uses asyncio.run(). Therefore, there were two viable paths forward: deprecate and remove the implicit creation of the event loop in get_event_loop(), or abandon asyncio.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 on asyncio.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 use asyncio.run(): If you call asyncio.set_event_loop(asyncio.new_event_loop()) at the top of your program, you can continue to use get_event_loop as before, provided you also start the event loop with get_event_loop().run_forever() and do not use asyncio.run().

  3. SoundsSerious commented on Apr 29, 2026

    @SoundsSerious
    Author

    @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

    I was tagged into this conversation via https://github.com/python/cpython/issues/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 use asyncio.run(): If you call asyncio.set_event_loop(asyncio.new_event_loop()) at the top of your program, you can continue to use get_event_loop as before, provided you also start the event loop with get_event_loop().run_forever() and do not use asyncio.run().
    

    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, unless LoopSingleton is 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.run seems like it should be more of a create_task + run_until_complete oriented workflow or am I missing something. Its a convenient way to run and wait on async code. If an application wants to keep the asyncio.run event-shutdown behavior that function can get a do_shutdown argument for backwards compatibility.

  4. added
    stdlibStandard Library Python modules in the Lib/ directory
    and removed
    type-bugAn unexpected behavior, bug, or error
    on Apr 29, 2026
  5. picnixz commented on Apr 29, 2026

    @picnixz
    Member

    cc @kumaraditya303

    I am removing the bug label because I think this was already discussed but feel free to add it back Kumar.

  6. added
    docsDocumentation in the Doc dir
    pendingThe issue will be closed if no feedback is provided
    on Apr 29, 2026
  7. kumaraditya303 commented on May 12, 2026

    @kumaraditya303
    Contributor

    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_loop to ease migration which you can still use. Otherwise this doesn't seem actionable to me so I'll close this.

  8. moved this from Todo to Done in asyncioon May 12, 2026
  9. SoundsSerious commented on May 12, 2026

    @SoundsSerious
    Author

    @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 event loop is the place where most interoperability occurs. ... In order to support both directions of adaptation, two separate APIs are specified: An interface for managing the current event loop / The interface of a conforming event loop

    The same PEP documents lazy event-loop creation as the by-design behavior of the default policy, not as a transitional convenience:

    Event loop by the default policy creation is lazy; i.e. the first call to get_event_loop() creates an event loop instance if necessary and specified by the current policy.

    And it explicitly contemplates third-party library code calling get_event_loop():

    a third party module may well implement a new transport and provide a constructor or factory function for it that simply takes an event loop as an argument or calls get_event_loop().

    The 3.10–3.14 deprecation removes this interoperability primitive. The replacement APIs — asyncio.run() and get_running_loop() — cover entry-point lifecycle and intra-coroutine loop access. Neither covers the cross-framework integration case PEP 3156 explicitly named and prescribed get_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.

  10. kumaraditya303 commented on May 13, 2026

    @kumaraditya303
    Contributor

    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.

    PEPs are historical documents and do not reflect the current status. set_event_loop is not deprecated and is part of the public API, it is not going anywhere and is supported.

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

    docsDocumentation in the Doc dirpendingThe issue will be closed if no feedback is providedstdlibStandard Library Python modules in the Lib/ directorytopic-asyncio

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions