python/cpython · error · ValueError

{obj!r}.__annotations__ is neither a dict nor None

Error message

{obj!r}.__annotations__ is neither a dict nor None

What it means

_get_dunder_annotations() validates that obj.__annotations__, when present, is either a dict or None; any other type raises ValueError. This catches objects (or classes) whose __annotations__ was overwritten with, say, a list, a string, or a property, which would otherwise silently corrupt downstream consumers.

Source

Thrown at Lib/annotationlib.py:1162

    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)
        except AttributeError:
            # For static types, the descriptor raises AttributeError.
            return None
    else:
        ann = getattr(obj, "__annotations__", None)
        if ann is None:
            return None

    if not isinstance(ann, dict):
        raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None")
    return ann


class _ExtraNameFixer(ast.NodeTransformer):
    """Fixer for __extra_names__ items in ForwardRef __repr__ and string evaluation"""
    def __init__(self, extra_names):
        self.extra_names = extra_names

    def visit_Name(self, node: ast.Name):
        if (new_name := self.extra_names.get(node.id, _sentinel)) is not _sentinel:
            node = ast.Name(id=type_repr(new_name))
        return node

View on GitHub (pinned to bc6749cc3b)

Solutions

  1. Set __annotations__ to a plain dict: obj.__annotations__ = {'x': int}.
  2. Delete the bogus attribute (del obj.__annotations__) so the lookup returns None and get_annotations falls back to __annotate__ or {}.
  3. Validate with isinstance(obj.__annotations__, (dict, type(None))) before calling annotation APIs.

Example fix

# before
class C: pass
C.__annotations__ = ['x', 'y']  # later: ValueError: ...__annotations__ is neither a dict nor None

# after
C.__annotations__ = {'x': int, 'y': str}
Defensive patterns

Strategy: validation

Validate before calling

def annotations_ok(obj) -> bool:
    ann = getattr(obj, '__annotations__', None)
    return ann is None or isinstance(ann, dict)

Type guard

def has_dict_annotations(obj) -> bool:
    return isinstance(getattr(obj, '__annotations__', None), dict)

Try / catch

try:
    ann = get_annotations(obj)
except ValueError as e:
    if 'neither a dict nor None' in str(e):
        object.__setattr__(obj, '__annotations__', {})  # repair then retry
        ann = get_annotations(obj)
    else:
        raise

Prevention

When it happens

Trigger: instance.__annotations__ = 'x: int'; a class body assigning __annotations__ = [] ; a mock or dataclass-like tool setting __annotations__ to a non-dict; a property named __annotations__ returning a tuple.

Common situations: Dynamic annotation injection by ORMs/serializers that writes the wrong type; patching in tests; pickling/copying artifacts that restore __annotations__ as another structure; typos assigning a tuple of keys.

Related errors


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