python/cpython · error · ValueError
deprecated-removed:: second argument cannot be `next`
Error message
deprecated-removed:: second argument cannot be `next`
What it means
BaseSelectorEventLoop.close() (Unix/selector-based loops) refuses to close a loop that is currently running, mirroring the base-class contract: close only after run_forever()/run_until_complete() returns. Closing a live loop would destroy its selector and self-pipe while callbacks still execute, so it raises this RuntimeError as a guard.
Source
Thrown at Doc/tools/extensions/changes.py:57
class DeprecatedRemoved(VersionChange):
required_arguments = 2
_deprecated_label = sphinx_gettext(
"Deprecated since version %s, will be removed in version %s"
)
_removed_label = sphinx_gettext(
"Deprecated since version %s, removed in version %s"
)
def run(self) -> list[Node]:
# Replace the first two arguments (deprecated version and removed version)
# with a single tuple of both versions.
version_deprecated = expand_version_arg(
self.arguments[0], self.config.release
)
version_removed = self.arguments.pop(1)
if version_removed == "next":
raise ValueError(
"deprecated-removed:: second argument cannot be `next`"
)
self.arguments[0] = version_deprecated, version_removed
# Set the label based on if we have reached the removal version
current_version = tuple(map(int, self.config.version.split(".")))
removed_version = tuple(map(int, version_removed.split(".")))
if current_version < removed_version:
versionlabels[self.name] = self._deprecated_label
versionlabel_classes[self.name] = "deprecated"
else:
versionlabels[self.name] = self._removed_label
versionlabel_classes[self.name] = "removed"
try:
return super().run()
finally:
# reset versionlabels and versionlabel_classes
versionlabels[self.name] = ""View on GitHub (pinned to bc6749cc3b)
Solutions
- Call loop.close() only after the run call has returned (run_until_complete/run_forever + stop)
- From inside a coroutine, end by stopping the loop (loop.stop()) or simply return and let the driver close it
- Use asyncio.run() so close-ordering is handled correctly
- For loops in other threads, request stop via loop.call_soon_threadsafe(loop.stop), join the thread, then close
Example fix
# before
async def main(loop):
await work()
loop.close() # RuntimeError: still running
# after
async def main():
await work()
loop.run_until_complete(main())
loop.close() Defensive patterns
Strategy: validation
Validate before calling
def shutdown_loop(loop, loop_thread=None):
if loop.is_running():
loop.call_soon_threadsafe(loop.stop)
if loop_thread:
loop_thread.join(timeout=5)
if not loop.is_closed():
loop.close() Try / catch
try:
loop.close()
except RuntimeError as e:
if 'running event loop' in str(e):
loop.stop()
return # retry close after run_forever() returns
raise Prevention
- Use asyncio.run() instead of manual loop lifecycle where possible
- Never close a loop from its own coroutine or callback
- Stop cross-thread loops via call_soon_threadsafe(loop.stop) then join
- Order teardown: stop -> run returns -> close
When it happens
Trigger: Calling loop.close() from a coroutine, a task, or a callback scheduled on the running loop; closing the loop from a signal handler while run_forever() is active; teardown in pytest fixtures executed while a loop thread is still running the loop.
Common situations: Application shutdown code in finally blocks or atexit that closes the loop before the runner thread stopped; mixing asyncio.run with manual loop management; background-thread loops closed by the main thread without stopping them first.
Related errors
- {htmldir!r} is not a Sphinx HTML output directory
- Object type mismatch in limited API annotation for {name}: {
- {name} is not part of stable ABI. Document it as `c:macro::`
- profiling_trace: Placeholder pattern not found in {js_path.n
- no running event loop
AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14).
Data as JSON: /api/errors/cf0f882f84050b5e.
Report an issue: GitHub.