Pewarta Proxy

📋 Server Request & Proxy Logs

Loading active proxy settings...
Developed by Khairul Adha (adhexa.id)
Mengambil log server...

⚙️ Pengaturan Proxy Target

Skip to content

annotationlib.ForwardRef.evaluate() ignores explicit globals/locals when the reference holds a closure cell #160948

Description

@arpittkhandelwal

Bug report

Bug description

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:

from annotationlib import Format, ForwardRef, get_annotations

def outer():
    def inner(a: x):
        pass
    fr = get_annotations(inner, format=Format.FORWARDREF)["a"]
    x = 99
    return fr

fr = outer()
print(fr.evaluate())                    # 99 (correct)
print(fr.evaluate(locals={"x": 42}))    # 99  ← expected 42
print(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.

CPython versions tested on:

3.16

Operating systems tested on:

No response

Linked PRs

Activity

  1. added a commit that references this issue on Oct 30, 2026
  2. picnixz commented on Oct 30, 2026

    @picnixz
    Member

    I think I already saw that issue elsewhere. Can you look at past issues?

  3. DavidCEllis commented on Oct 30, 2026

    @DavidCEllis
    Contributor
    print(fr.evaluate(globals={"x": 43}))   # 99  ← expected 43

    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.

    from annotationlib import get_annotations, Format
    
    def outer():
        def inner(a: unknown[x]):
            pass
        fr = get_annotations(inner, format=Format.FORWARDREF)["a"]
        unknown = list  # define the generic here
        x = 99
        return fr
    
    fr = outer()
    print("Replacing globals:", fr.evaluate(globals={'x': 42}))
    print("Replacing locals:", fr.evaluate(locals={'x': 43}))
    Replacing globals: list[99]
    Replacing locals: list[43]
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytopic-typingtype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions