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

  1. Call loop.close() only after the run call has returned (run_until_complete/run_forever + stop)
  2. From inside a coroutine, end by stopping the loop (loop.stop()) or simply return and let the driver close it
  3. Use asyncio.run() so close-ordering is handled correctly
  4. 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

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


AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14). Data as JSON: /api/errors/cf0f882f84050b5e. Report an issue: GitHub.