python/cpython · error · ValueError

Object type mismatch in limited API annotation for {name}: {

Error message

Object type mismatch in limited API annotation for {name}: {ROLE_TO_OBJECT_TYPE[record.role]!r} != {objtype!r}

What it means

Runner._lazy_init() raises RuntimeError when the Runner has already been closed (used as a context manager and exited, or close() called explicitly). Any later attempt to run() or get_loop() re-initializes lazily and hits this state check. A closed Runner is terminal and must be replaced by a new instance.

Source

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

        if not par[0].get("ids", None):
            continue
        name = par[0]["ids"][0].removeprefix("c.")
        objtype = par["objtype"]

        # Thread safety annotation — inserted first so it appears last (bottom-most)
        # among all annotations.
        if entry := threadsafety_data.get(name):
            annotation = _threadsafety_annotation(entry.level)
            node.insert(0, annotation)

        # Stable ABI annotation.
        if record := stable_abi_data.get(name):
            if ROLE_TO_OBJECT_TYPE[record.role] != objtype:
                msg = (
                    f"Object type mismatch in limited API annotation for {name}: "
                    f"{ROLE_TO_OBJECT_TYPE[record.role]!r} != {objtype!r}"
                )
                raise ValueError(msg)
            annotation = _stable_abi_annotation(record)
            node.insert(0, annotation)

        # Unstable API annotation.
        if name.startswith("PyUnstable"):
            annotation = _unstable_api_annotation()
            node.insert(0, annotation)

        # Return value annotation
        if objtype != "function":
            continue
        if name not in refcount_data:
            continue
        entry = refcount_data[name]
        if not entry.result_type.endswith("Object*"):
            continue
        annotation = _return_value_annotation(entry.result_refs)
        node.insert(0, annotation)

View on GitHub (pinned to bc6749cc3b)

Solutions

  1. Create a fresh Runner for each batch of runs: keep runs inside one 'with' block, or construct a new Runner after close
  2. Remove premature close() calls in error handlers; rely on context-manager exit instead
  3. If reusing across calls, instantiate the Runner without 'with' and close it only in final shutdown, checking is_closed if available
  4. Restructure to asyncio.run() per invocation when runner reuse is not actually needed

Example fix

# before
with Runner() as r:
    r.run(first())
r.run(second())  # Runner is closed
# after
with Runner() as r:
    r.run(first())
    r.run(second())  # both inside the context
Defensive patterns

Strategy: validation

Validate before calling

class ReusableRunner:
    def __init__(self):
        self._runner = None

    def run(self, coro):
        if self._runner is None or self._runner._state is _State.CLOSED:
            self._runner = asyncio.Runner()
        return self._runner.run(coro)

Try / catch

try:
    r.run(coro())
except RuntimeError as e:
    if 'Runner is closed' in str(e):
        r = asyncio.Runner()          # recreate and retry once
        return r.run(coro())
    raise

Prevention

When it happens

Trigger: Calling run() twice on a Runner used in a 'with' block after the block exited: with Runner() as r: r.run(a); r.run(b). Also storing a Runner across requests and reusing it after an explicit r.close() in error handling, or calling get_loop() post-close.

Common situations: Refactoring code from a single asyncio.run() to a shared Runner and keeping the 'with' scoping; error paths that close() the runner then fall through to retry logic that runs again; caching a Runner as a module global while a shutdown hook closes it.

Related errors


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