python/cpython · error · ValueError
{obj!r}.__annotate__ returned a non-dict
Error message
{obj!r}.__annotate__ returned a non-dict What it means
_get_and_call_annotate() verifies that obj.__annotate__(format) returns a dict; anything else (list, None, MappingProxy, custom Mapping) raises ValueError. The PEP 649 contract requires __annotate__ to return a real dict of annotation name to value, so a non-dict signals a broken custom implementation.
Source
Thrown at Lib/annotationlib.py:1134
"""If the given argument annotation expression is a star unpack e.g. `'*Ts'`
rewrite it to a valid expression.
"""
if arg.lstrip().startswith("*"):
return f"({arg},)[0]" # E.g. (*Ts,)[0] or (*tuple[int, int],)[0]
else:
return arg
def _get_and_call_annotate(obj, format):
"""Get the __annotate__ function and call it.
May not return a fresh dictionary.
"""
annotate = getattr(obj, "__annotate__", None)
if annotate is not None:
ann = call_annotate_function(annotate, format, owner=obj)
if not isinstance(ann, dict):
raise ValueError(f"{obj!r}.__annotate__ returned a non-dict")
return ann
return None
_BASE_GET_ANNOTATIONS = type.__dict__["__annotations__"].__get__
def _get_dunder_annotations(obj):
"""Return the annotations for an object, checking that it is a dictionary.
Does not return a fresh dictionary.
"""
# This special case is needed to support types defined under
# from __future__ import annotations, where accessing the __annotations__
# attribute directly might return annotations for the wrong class.
if isinstance(obj, type):
try:
ann = _BASE_GET_ANNOTATIONS(obj)View on GitHub (pinned to bc6749cc3b)
Solutions
- Make the custom __annotate__ return dict(...), e.g. `return {'x': int}` — convert Mappings with dict().
- Ensure decorators around __annotate__ preserve the return value or wrap with @wraps and return the inner dict.
- Catch ValueError when introspecting untrusted objects and fall back to __annotations__.
Example fix
# before
def __annotate__(format):
return [('x', int)] # ValueError: ...__annotate__ returned a non-dict
# after
def __annotate__(format):
return {'x': int} Defensive patterns
Strategy: try-catch
Validate before calling
def annotate_returns_dict(obj) -> bool:
ann_fn = getattr(obj, '__annotate__', None)
if ann_fn is None:
return True
from annotationlib import Format
try:
return isinstance(ann_fn(Format.STRING), dict)
except Exception:
return False Type guard
def has_dict_annotate(obj) -> bool:
fn = getattr(obj, '__annotate__', None)
return fn is None or getattr(fn, '__annotations__', {}).get('return') in (dict, None) Try / catch
try:
ann = get_annotations(obj, format=Format.STRING)
except ValueError as e:
if 'returned a non-dict' in str(e):
ann = dict(getattr(obj, '__annotations__', {}) or {})
else:
raise Prevention
- Always return a plain dict from custom __annotate__
- Convert Mapping types with dict() before returning
- Unit-test custom __annotate__ return types across formats
When it happens
Trigger: A class defines __annotate__ returning None, a list, or a collections.abc.Mapping subclass; a decorator wraps __annotate__ and accidentally returns the wrapper's value; a metaclass injects a malformed __annotate__.
Common situations: Frameworks synthesizing __annotate__ dynamically; test mocks with loose return_value; migrating from __annotations__-based hacks to __annotate__ incorrectly.
Related errors
- annotate function does not support VALUE format
- Cannot stringify annotation containing string formatting
- The VALUE_WITH_FAKE_GLOBALS format is for internal use only
- write() returned incorrect number of bytes
- Forward reference must be an expression -- got {arg!r}
AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14).
Data as JSON: /api/errors/9bdd573e985f077c.
Report an issue: GitHub.