Skip to content

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

@SoundsSerious

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

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

    Status

    Todo

    Status

    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions