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
- Move loop.close() to after the run call completes: run_until_complete(...) then loop.close()
- Inside a coroutine, just return or stop the loop with loop.stop() and close it after run_forever() returns
- Prefer asyncio.run(main()) which handles creation, running, and closing in the correct order
- 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
- Prefer asyncio.run() so close ordering is always correct
- Never close a loop from inside its own coroutines or callbacks
- Stop loops via loop.stop() and close after the run call returns
- In tests, close fixtures after run_until_complete fully unwinds
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
- deprecated-removed:: second argument cannot be `next`
- Failed to fetch glossary.json
- d3-flame-graph library failed to load
- Not implemented
- Object type mismatch in limited API annotation for {name}: {
AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14).
Data as JSON: /api/errors/a18cff1bc6998ad9.
Report an issue: GitHub.