python/cpython · error · ValueError

{htmldir!r} is not a Sphinx HTML output directory

Error message

{htmldir!r} is not a Sphinx HTML output directory

What it means

BaseEventLoop.close() (here via the proactor event loop on Windows) refuses to close a loop that is currently running. A loop may only be closed after run_forever()/run_until_complete() has returned. Closing a running loop would tear down primitives the loop still needs, so asyncio guards it with this RuntimeError.

Source

Thrown at Doc/tools/check-html-ids.py:45

    def handle_starttag(self, tag, attrs):
        for name, value in attrs:
            if name == 'id':
                if not IGNORED_ID_RE.fullmatch(value):
                    self.__ids.add(value)


def get_ids_from_file(path):
    ids = set()
    gatherer = IDGatherer(ids)
    with path.open(encoding='utf-8') as file:
        while chunk := file.read(4096):
            gatherer.feed(chunk)
    return ids


def gather_ids(htmldir, *, verbose_print):
    if not htmldir.joinpath('objects.inv').exists():
        raise ValueError(f'{htmldir!r} is not a Sphinx HTML output directory')

    if sys._is_gil_enabled:
        pool = concurrent.futures.ProcessPoolExecutor()
    else:
        pool = concurrent.futures.ThreadPoolExecutor()
    tasks = {}
    for path in htmldir.glob('**/*.html'):
        relative_path = path.relative_to(htmldir)
        if '_static' in relative_path.parts:
            continue
        if 'whatsnew' in relative_path.parts:
            continue
        tasks[relative_path] = pool.submit(get_ids_from_file, path=path)

    ids_by_page = {}
    for relative_path, future in tasks.items():
        verbose_print(relative_path)
        ids = future.result()

View on GitHub (pinned to bc6749cc3b)

Solutions

  1. Move loop.close() to after the run call completes: run_until_complete(...) then loop.close()
  2. Inside a coroutine, just return or stop the loop with loop.stop() and close it after run_forever() returns
  3. Prefer asyncio.run(main()) which handles creation, running, and closing in the correct order
  4. For cross-thread teardown, signal the loop to stop via loop.call_soon_threadsafe(loop.stop), join the runner thread, then close

Example fix

# before
async def main(loop):
    ...
    loop.close()  # RuntimeError: loop is running
loop.run_until_complete(main(loop))
# after
async def main():
    ...
loop.run_until_complete(main())
loop.close()
Defensive patterns

Strategy: validation

Validate before calling

def stop_and_close(loop):
    if loop.is_running():
        loop.call_soon_threadsafe(loop.stop)
        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()  # close from outside after run returns
    else:
        raise

Prevention

When it happens

Trigger: Calling loop.close() from inside a coroutine or callback running on that same loop; calling loop.close() on the loop's runner thread while run_forever() is still blocked; closing a loop from another thread while it runs. Common in tests that tear down in a finally block that executes before run_until_complete unwinds.

Common situations: Cleanup code (finally/atexit/signal handlers) that closes the loop regardless of run state; nesting mistakes where asyncio.run(...) is emulated manually and close() is invoked inside main(); pytest fixtures closing a loop that a background thread is still running.

Related errors


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