{"record":{"id":"9bdd573e985f077c","repo":"python/cpython","slug":"obj-r-annotate-returned-a-non-dict","errorCode":null,"errorMessage":"{obj!r}.__annotate__ returned a non-dict","messagePattern":"(.+?)\\.__annotate__ returned a non-dict","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"Lib/annotationlib.py","lineNumber":1134,"sourceCode":"    \"\"\"If the given argument annotation expression is a star unpack e.g. `'*Ts'`\n       rewrite it to a valid expression.\n       \"\"\"\n    if arg.lstrip().startswith(\"*\"):\n        return f\"({arg},)[0]\"  # E.g. (*Ts,)[0] or (*tuple[int, int],)[0]\n    else:\n        return arg\n\n\ndef _get_and_call_annotate(obj, format):\n    \"\"\"Get the __annotate__ function and call it.\n\n    May not return a fresh dictionary.\n    \"\"\"\n    annotate = getattr(obj, \"__annotate__\", None)\n    if annotate is not None:\n        ann = call_annotate_function(annotate, format, owner=obj)\n        if not isinstance(ann, dict):\n            raise ValueError(f\"{obj!r}.__annotate__ returned a non-dict\")\n        return ann\n    return None\n\n\n_BASE_GET_ANNOTATIONS = type.__dict__[\"__annotations__\"].__get__\n\n\ndef _get_dunder_annotations(obj):\n    \"\"\"Return the annotations for an object, checking that it is a dictionary.\n\n    Does not return a fresh dictionary.\n    \"\"\"\n    # This special case is needed to support types defined under\n    # from __future__ import annotations, where accessing the __annotations__\n    # attribute directly might return annotations for the wrong class.\n    if isinstance(obj, type):\n        try:\n            ann = _BASE_GET_ANNOTATIONS(obj)","sourceCodeStart":1116,"sourceCodeEnd":1152,"githubUrl":"https://github.com/python/cpython/blob/bc6749cc3b5ae4a5e88a6cc2d5b3bebbe354eae6/Lib/annotationlib.py#L1116-L1152","documentation":"_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.","triggerScenarios":"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__.","commonSituations":"Frameworks synthesizing __annotate__ dynamically; test mocks with loose return_value; migrating from __annotations__-based hacks to __annotate__ incorrectly.","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__."],"exampleFix":"# before\ndef __annotate__(format):\n    return [('x', int)]  # ValueError: ...__annotate__ returned a non-dict\n\n# after\ndef __annotate__(format):\n    return {'x': int}","handlingStrategy":"try-catch","validationCode":"def annotate_returns_dict(obj) -> bool:\n    ann_fn = getattr(obj, '__annotate__', None)\n    if ann_fn is None:\n        return True\n    from annotationlib import Format\n    try:\n        return isinstance(ann_fn(Format.STRING), dict)\n    except Exception:\n        return False","typeGuard":"def has_dict_annotate(obj) -> bool:\n    fn = getattr(obj, '__annotate__', None)\n    return fn is None or getattr(fn, '__annotations__', {}).get('return') in (dict, None)","tryCatchPattern":"try:\n    ann = get_annotations(obj, format=Format.STRING)\nexcept ValueError as e:\n    if 'returned a non-dict' in str(e):\n        ann = dict(getattr(obj, '__annotations__', {}) or {})\n    else:\n        raise","preventionTips":["Always return a plain dict from custom __annotate__","Convert Mapping types with dict() before returning","Unit-test custom __annotate__ return types across formats"],"tags":["python","annotations","pep649","contract-violation"],"backgroundTag":null,"analyzedSha":"bc6749cc3b5ae4a5e88a6cc2d5b3bebbe354eae6","analyzedAt":"2026-08-14T22:01:13.976Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}