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
- Create a fresh Runner for each batch of runs: keep runs inside one 'with' block, or construct a new Runner after close
- Remove premature close() calls in error handlers; rely on context-manager exit instead
- If reusing across calls, instantiate the Runner without 'with' and close it only in final shutdown, checking is_closed if available
- 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
- Keep every run() inside the Runner's with-block
- Do not close() runners on per-request errors; reserve close for final shutdown
- Store runners in explicit lifecycle containers rather than ad-hoc globals
- Prefer one asyncio.run() per process entrypoint unless runner reuse is required
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
- {htmldir!r} is not a Sphinx HTML output directory
- Unknown thread safety level {level!r} for {name!r}. Valid le
- deprecated-removed:: second argument cannot be `next`
- Creating _SSLProtocolTransport twice
- cannot switch state from {} to {}
AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14).
Data as JSON: /api/errors/6f22681a2b254b23.
Report an issue: GitHub.