You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
annotationlib.ForwardRef.evaluate() ignores explicit globals/locals when the reference holds a closure cell #160948
When a ForwardRef obtained from annotationlib.get_annotations(..., format=Format.FORWARDREF) refers to a single name from an enclosing
function scope, it stores the value in a closure cell (__cell__). ForwardRef.evaluate() returns that cell value immediately, before
looking at the globals and locals arguments — so explicitly passed
namespaces are silently ignored:
fromannotationlibimportFormat, ForwardRef, get_annotationsdefouter():
definner(a: x):
passfr=get_annotations(inner, format=Format.FORWARDREF)["a"]
x=99returnfrfr=outer()
print(fr.evaluate()) # 99 (correct)print(fr.evaluate(locals={"x": 42})) # 99 ← expected 42print(fr.evaluate(globals={"x": 43})) # 99 ← expected 43# A plain ForwardRef without a cell honours the explicit namespaces:print(ForwardRef("x").evaluate(locals={"x": 42})) # 42
This is inconsistent with every other resolution path in the same
method: for a plain ForwardRef, and for references holding multiple
cells (a dict of cells), explicitly passed locals win over the
captured scope. Only the single-cell path flips the precedence.
The documented contract points the other way as well: ForwardRef.evaluate() documents that "The arguments to this method
can be used to provide bindings for names that would otherwise be
undefined" and that "globals and locals are passed to eval(),
representing the global and local namespaces in which the name is
evaluated" (Doc/library/annotationlib.rst).
Expected behaviour: explicitly passed globals/locals take
precedence over the closure cell, as they do on the other paths; the
cell value is used as a fallback when the name is not provided
explicitly (and when no namespaces are passed at all, behaviour is
unchanged).
I have a fix and regression tests ready and will open a PR.
Environment
CPython main (3.16.0a1+dev, 83fe630), built locally; annotationlib
was added in 3.14, and the single-cell shortcut in ForwardRef.evaluate() is present there in the same form.
The lookup should check the closure before looking at globals, you can see this behaviour if you put your value inside a generic forward reference where the nonlocals are placed between the globals and locals.
Correction to the expected behaviour in the report above, following the discussion here: the globals part was wrong. As @DavidCEllis showed, on the multi-name path the nonlocals already sit between the locals and the globals, so fr.evaluate(globals={"x": 43}) returning 99 is correct. The bug is only that an explicitly passed locals is ignored: fr.evaluate(locals={"x": 42}) should return 42. PR #160952 has been updated to change only the locals handling.
Activity
pythongh-160948: Fix annotationlib.ForwardRef.evaluate() ignoring exp…
picnixz commentedon Oct 30, 2026
I think I already saw that issue elsewhere. Can you look at past issues?
DavidCEllis commentedon Oct 30, 2026
I'd still expect 99 here.
The lookup should check the closure before looking at globals, you can see this behaviour if you put your value inside a generic forward reference where the nonlocals are placed between the globals and locals.
picnixz commentedon Oct 31, 2026
The
globalssuggestion also looks incorrect to me. Otheriwse, the local change seems relevant.pythongh-160948: Only an explicitly passed locals takes precedence ov…
arpittkhandelwal commentedon Oct 31, 2026
Correction to the expected behaviour in the report above, following the discussion here: the
globalspart was wrong. As @DavidCEllis showed, on the multi-name path the nonlocals already sit between the locals and the globals, sofr.evaluate(globals={"x": 43})returning 99 is correct. The bug is only that an explicitly passedlocalsis ignored:fr.evaluate(locals={"x": 42})should return 42. PR #160952 has been updated to change only thelocalshandling.