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

  1. Make the custom __annotate__ return dict(...), e.g. `return {'x': int}` — convert Mappings with dict().
  2. Ensure decorators around __annotate__ preserve the return value or wrap with @wraps and return the inner dict.
  3. 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

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


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