python/cpython · error · LookupError

{name} is not part of stable ABI. Document it as `c:macro::`

Error message

{name} is not part of stable ABI. Document it as `c:macro::` rather than `corresponding-type-slot::`.

What it means

asyncio.run() must be the entry point that creates and owns the event loop; it cannot run while a loop is already running in the current thread. The guard uses events._get_running_loop() and deliberately fails fast with a short traceback. Nesting asyncio.run() inside a coroutine is unsupported because the outer loop cannot be suspended behind a second loop.

Source

Thrown at Doc/tools/extensions/c_annotations.py:428

    If there is no corresponding field, these should be documented as normal
    macros.
    """

    has_content = False

    required_arguments = 1
    optional_arguments = 0

    def run(self) -> list[nodes.Node]:
        name = self.arguments[0]
        state = self.env.domaindata["c_annotations"]
        stable_abi_data = state["stable_abi_data"]

        try:
            record = stable_abi_data[name]
        except LookupError as err:
            raise LookupError(
                f"{name} is not part of stable ABI. "
                + "Document it as `c:macro::` rather than "
                + "`corresponding-type-slot::`."
            ) from err

        annotation = _stable_abi_annotation(record, is_corresponding_slot=True)

        node = nodes.paragraph()
        content = [
            ".. c:namespace:: NULL",
            "",
            ".. c:macro:: " + name,
            "   :no-typesetting:",
        ]
        self.state.nested_parse(StringList(content), 0, node)
        node.insert(0, annotation)
        return [node]

View on GitHub (pinned to bc6749cc3b)

Solutions

  1. Inside async code, await the coroutine directly instead of asyncio.run()
  2. Give the library an async API: async def do_async() plus a thin sync wrapper do() = asyncio.run(do_async()) that is only called from sync contexts
  3. In Jupyter, use top-level await or nest_asyncio.apply() as an environment-specific workaround
  4. Restructure so asyncio.run() appears exactly once at the true process entrypoint

Example fix

# before
async def handler():
    result = asyncio.run(fetch())  # RuntimeError
# after
async def handler():
    result = await fetch()
Defensive patterns

Strategy: validation

Validate before calling

import asyncio, asyncio.events as events

def run_bridge(coro_factory):
    try:
        events.get_running_loop()
    except RuntimeError:
        return asyncio.run(coro_factory())
    raise RuntimeError('already inside a loop: await the coroutine instead')

Try / catch

try:
    asyncio.run(main())
except RuntimeError as e:
    if 'running event loop' in str(e):
        raise RuntimeError('await main() directly here') from e
    raise

Prevention

When it happens

Trigger: Calling asyncio.run(...) inside an async def, inside a loop callback, inside a Jupyter notebook cell, or from a thread that is currently running a loop. Also transitively: a sync library function that internally calls asyncio.run() being invoked from async code.

Common situations: Jupyter/IPython (running loop) calling libraries that use asyncio.run() for bridging; test code mixing pytest.sync execution with asyncio.run inside fixtures; web frameworks (FastAPI handlers) calling sync SDKs that call asyncio.run(); migration from threads to asyncio where nested entrypoints remain.

Related errors


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