Unexpected behaviour related to PEP 563 (Postponed Evaluation of Annotations)

Hi!

We have a Python library that parses function annotations and uses this information to resolve the corresponding function overloads defined in another language (e.g.: C++, CUDA).

This has been working well, and we’re now in the process of adding support for from __future__ import annotations, however we hit a small bump in the road: string representations of type hints don’t evaluate to their corresponding type if they refer to a variable from a parent scope.

Take the following snippet as an example.

from __future__ import annotations

import inspect

class FooData:
    value: float

class Foo:
    Data = FooData

def create_fn(foo: Foo):
    def fn(data: foo.Data):
        # foo
        pass

    return fn

fn = create_fn(Foo())

try:
    print(inspect.get_annotations(fn, eval_str=True))
except NameError as e:
    print(f"inspect.get_annotations() failed with: {e}")

print(f"'foo' in fn.__code__.co_freevars ? {'foo' in fn.__code__.co_freevars}")

try:
    fn_locals = dict(zip(fn.__code__.co_freevars, (x.cell_contents for x in (fn.__closure__ or ()))))
    print(eval("foo.Data", globals(), fn_locals))
except NameError as e:
    print(f"eval() failed with: {e}")

As it stands, the type hint foo.Data doesn’t get resolved as expected: inspect.get_annotations() fails with eval_str=True, and the variable foo isn’t found in fn.__code__.co_freevars, so we can’t manually evaluate the type by passing the dictionary of closure variables to eval().

However, if we reference foo within the inner function (e.g.: by uncommenting the line from that snippet), then foo seems to get promoted to a closure variable since it now shows up in fn.__code__.co_freevars. Alas, inspect.get_annotations(eval_str=True) still fails because it doesn’t seem to account for such closure variables?

At first glance, this use case seems to be a valid pattern, so so we’re wondering if this is a known issue or if this could be the expected behavior?

Thank you!

  • foo.Data is not a valid type hint, which are at this point the primary intended purpose of annotations. foo is a variable and can therefore not appear inside of any valid type annotation.
  • Not supporting from __future__ import annotations is a perfectly valid option. At some point PEP 649 will be implemented and make these usecases possible to implement. (How exactly this will relate to the future import is still undecided)

Within the current semantics of python and from __future__ import annotations, it is completely impossible to evaluate this type expression after create_fn has returned since foo has been garbage collected: fn does not refer to it outside of the stringified annotation, meaning it doesn’t, as you noticed, get saved anywhere as a closure variable or similar.

1 Like

Awesome, thanks for the prompt and insightful reply, @MegaIng!