python/cpython · error · SyntaxError
Forward reference must be an expression -- got {arg!r}
Error message
Forward reference must be an expression -- got {arg!r} What it means
ForwardRef.__forward_code__ compiles the forward reference string with compile(..., 'eval') to build __code__. If the string is not a valid Python expression (e.g. it contains a statement like an assignment, or a lambda with ';'), the SyntaxError from compile is replaced by this clearer SyntaxError. It means the annotation string stored in a ForwardRef (typically from a quoted annotation or __annotations__ under PEP 563) cannot be evaluated later.
Source
Thrown at Lib/annotationlib.py:266
if names:
visitor = _ExtraNameFixer(names)
ast_expr = ast.parse(resolved_str, mode="eval").body
node = visitor.visit(ast_expr)
resolved_str = ast.unparse(node)
self.__resolved_str_cache__ = resolved_str
return self.__resolved_str_cache__
@property
def __forward_code__(self):
if self.__code__ is not None:
return self.__code__
arg = self.__forward_arg__
try:
self.__code__ = compile(_rewrite_star_unpack(arg), "<string>", "eval")
except SyntaxError:
raise SyntaxError(f"Forward reference must be an expression -- got {arg!r}")
return self.__code__
def __eq__(self, other):
if not isinstance(other, ForwardRef):
return NotImplemented
return (
self.__forward_arg__ == other.__forward_arg__
and self.__forward_module__ == other.__forward_module__
# Use "is" here because we use id() for this in __hash__
# because dictionaries are not hashable.
and self.__globals__ is other.__globals__
and self.__forward_is_class__ == other.__forward_is_class__
# Two separate cells are always considered unequal in forward refs.
and (
{name: id(cell) for name, cell in self.__cell__.items()}
== {name: id(cell) for name, cell in other.__cell__.items()}
if isinstance(self.__cell__, dict) and isinstance(other.__cell__, dict)
else self.__cell__ is other.__cell__View on GitHub (pinned to bc6749cc3b)
Solutions
- Fix the annotation string so it is a single valid Python expression (remove assignments, imports, semicolons).
- If the string comes from dynamic code, validate it first with ast.parse(s, mode='eval') and reject or repair non-expressions.
- Catch SyntaxError around ForwardRef.evaluate()/get_type_hints() and report which annotation string failed (the message includes the offending repr).
Example fix
// before
def f(x: "n = 10"): ...
typing.get_type_hints(f) # SyntaxError: Forward reference must be an expression -- got 'n = 10'
// after
def f(x: "int"): ...
typing.get_type_hints(f) # {'x': <class 'int'>} Defensive patterns
Strategy: try-catch
Validate before calling
import ast
def is_valid_forward_ref_expr(s: str) -> bool:
try:
ast.parse(s, mode='eval')
return True
except SyntaxError:
return False Type guard
from annotationlib import ForwardRef
def is_forward_ref(x) -> bool:
return isinstance(x, ForwardRef) Try / catch
from annotationlib import ForwardRef
try:
value = ref.evaluate()
except SyntaxError as e:
# e.g. log the offending annotation and skip it
print(f'skipping bad annotation: {e}') Prevention
- Keep quoted annotations as single expressions; never statements
- Validate generated annotation strings with ast.parse(mode='eval') before use
- Run typing.get_type_hints() in tests over all annotated modules to catch bad strings early
When it happens
Trigger: ForwardRef('x = 1').__forward_code__; ForwardRef('import os'); ForwardRef('x; y'); a quoted annotation like def f(x: "a = b") evaluated via ForwardRef.evaluate() or typing.get_type_hints(); annotations produced by broken source generation or manual string building.
Common situations: Hand-written string annotations containing statements instead of expressions; code generators emitting malformed annotation strings; get_type_hints() on modules whose annotations were tampered with; typo in a quoted annotation such as a stray '='.
Related errors
- Forward reference must be a string -- got {arg!r}
- Cannot subclass ForwardRef
- name '{name:.200}' is not defined
- Cannot stringify annotation containing string formatting
- The VALUE_WITH_FAKE_GLOBALS format is for internal use only
AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14).
Data as JSON: /api/errors/0108ed6e7340aa8c.
Report an issue: GitHub.