How do asyncio.get_running_loop() and asyncio.get_event_loop() differ, and which should library code call?
answer
- One is strict, one has a fallback
- Running loop versus the thread's installed loop
- The silent-creation fallback is gone
- Import-time capture caught the wrong loop
- Ask at the use site, inside async code
basics
~20 sasyncio.get_running_loop() returns the loop running in this thread and raises RuntimeError if there is none. asyncio.get_event_loop() also accepts the loop installed for the thread, and on Python 3.14 raises when none exists. Library code calls get_running_loop().
solid answer
~50 s`asyncio.get_running_loop()` has one job: return the loop that is running *right now* in this thread, or raise `RuntimeError('no running event loop')`. It never creates or installs anything, which is exactly why it is the call library and framework code should use — it can only succeed inside async context, and what it returns is by construction the loop your caller is on. `asyncio.get_event_loop()` returns that same running loop when there is one, but outside async context it falls back to whatever loop `asyncio.set_event_loop()` installed for the thread. For years it silently created a loop in that case, which let import-time code capture a loop nobody would ever run; 3.12 deprecated that and 3.14 removed it, so it now raises `RuntimeError: There is no current event loop in thread ...`. In practice: `asyncio.run()` at the entry point, `get_running_loop()` at the use site, and no module-level loop globals at all.
code
python · 12 linesimport asyncio
async def show():
# inside a running loop the two lookups agree
print(asyncio.get_running_loop() is asyncio.get_event_loop())
try:
asyncio.get_event_loop() # nothing running, nothing installed
except RuntimeError as exc:
print("at import time:", exc) # Python 3.14 raises instead of creating one
asyncio.run(show())go deeper
Know that get_running_loop() returns the loop you are running on and errors if you are not inside async code, and that you rarely need either call — asyncio.run() plus await covers most work.
Explain the running-loop versus thread-installed-loop distinction, why get_event_loop's old create-on-demand behaviour was dangerous, and that on 3.14 it raises RuntimeError instead.
Show the failure mode you have actually debugged: a loop captured at import time that nothing ever runs, callbacks that silently never fire, and symptoms that flip with import order. Argue for get_running_loop at the use site.
Own the convention: one loop per process created by the entry point, no loop globals anywhere, libraries that never install a loop for their caller, and a migration story for legacy modules that still hold one.
**Two lookups that used to look interchangeable and never were.** Asyncio keeps at most one *running* loop per thread, and separately a *current* loop per thread — the one `asyncio.set_event_loop()` installed, which may or may not be running. The two lookup functions differ in which of those they will settle for. `asyncio.get_running_loop()` (3.7+) returns the running loop and nothing else. No running loop, no result: it raises `RuntimeError: no running event loop`. That strictness is the feature. Called from inside a coroutine, a callback, or anything the loop itself is driving, it is guaranteed to hand back the loop that is actually driving you, and it can never invent one. `asyncio.get_event_loop()` tries the running loop first and falls back to the thread's current loop. Historically, when there was neither, it would *create* one and install it — which is where the trouble lived. That fallback is gone: 3.12 emitted a `DeprecationWarning` when it had to create a loop, and 3.14 turns the no-loop case into an error. ```pycon >>> import asyncio >>> asyncio.get_event_loop() RuntimeError: There is no current event loop in thread 'MainThread'. ``` **Why the old fallback caused real bugs.** Consider an image-thumbnail worker whose queue module opened with a module-level `LOOP = asyncio.get_event_loop()` and scheduled its retry callbacks on `LOOP`. On old versions that line ran at import time and quietly manufactured loop *A*. Later, `asyncio.run(main())` created loop *B* and ran everything on it. Callbacks scheduled on *A* never fired, because nothing ever turned *A*'s crank — and the failure was invisible: no exception, no warning, just work that silently never happened. The bug was also order-dependent in the worst way. Breaking a circular import at startup by moving an import into a function changed *when* the module was first imported, which changed which loop the global captured, which flipped the symptom on and off between deployments. Making the no-loop case raise converts that class of bug into an immediate, obvious startup crash. **Inside a running loop the two agree.** Both return the same object, so the argument is never about the value; it is about what happens outside async context. `get_running_loop()` fails loudly there, which is what you want in a library that has no business creating loops for its caller. ```python import asyncio async def show(): print(asyncio.get_running_loop() is asyncio.get_event_loop()) # True asyncio.run(show()) ``` **What replaced the old habits.** Storing a loop in a global was often done so objects could be told which loop to use. That need is gone: the `loop` parameter was removed from asyncio's synchronization primitives and queues in 3.10, and they now bind to the running loop when they first need it. So the modern shape is that nothing holds a loop reference across the program; code that truly needs the loop object — to schedule a plain callback, or to hand work to an executor — calls `asyncio.get_running_loop()` at the moment it needs it, inside async context. **Threads that are not running a loop.** A worker thread that wants its own asyncio work does not fetch a loop from the main thread; it runs `asyncio.run()` itself, or explicitly builds one with `asyncio.new_event_loop()` and installs it for that thread with `asyncio.set_event_loop()`. Loops are per-thread and never shared: passing one thread's loop to another thread and calling its methods there is a race, since almost every loop method must be called from the loop's own thread. **The summary an interviewer wants:** `get_running_loop()` inside async code, `asyncio.run()` (or `asyncio.Runner`) at the process entry point, `set_event_loop()` only when you are deliberately setting up a loop for a thread, and `get_event_loop()` essentially never in new code — it survives mostly for compatibility with pre-3.7 patterns, and on 3.14 its remaining sharp edge is a `RuntimeError` rather than a silent wrong answer.
- Inside a running coroutine, do the two functions return the same loop object?Yes — `get_event_loop()` checks for a running loop first, so inside async context both return the identical object. The difference only shows outside async context, where `get_running_loop()` raises and `get_event_loop()` looks for the thread's installed loop.
- How does a worker thread get an event loop of its own?It runs `asyncio.run()` on that thread, or builds one with `asyncio.new_event_loop()` and installs it with `asyncio.set_event_loop()` before running it. Loops are per-thread: you never hand another thread's loop around, because almost every loop method must be called from the loop's own thread.
- Why is holding a loop reference in a module global no longer needed?Because asyncio objects stopped needing to be told a loop. The `loop` parameter was removed from the synchronization primitives and queues in 3.10, and they bind to the running loop when they first suspend somebody. Code that still needs the loop object asks for it at the use site with `get_running_loop()`.
saying these in an interview costs you the question
- Saying get_event_loop always creates a loop when none exists
- Treating the two lookups as interchangeable
- Caching a loop in a module global at import time
- Thinking get_running_loop works outside async context
- Calling set_event_loop before asyncio.run to prepare it
- Passing one thread's loop to another thread and using it there