Skip to content

Type annotations in execd code do not resolve as expected #145426

@vfazio

Description

@vfazio

Bug report

Bug description:

We've counted on the ability to exec code to test that various type annotations lead to the correct success/error scenarios

@pytest.mark.parametrize(
    "annotation, value, raises, match",
    [
        pytest.param("Annotated[int, at.Gt(1)]", "0", True, "0 is not greater than 1", id="Gt fail"),
        pytest.param("Annotated[int, at.Gt(1)]", "2", False, "", id="Gt ok"),
        pytest.param("Annotated[int, at.Lt(1)]", "2", True, "2 is not less than 1", id="Lt fail"),
        pytest.param("Annotated[int, at.Lt(1)]", "0", False, "", id="Lt ok"),
        pytest.param("Annotated[int, at.Ge(1)]", "0", True, "0 is not greater than or equal to 1", id="Ge fail"),
        pytest.param("Annotated[int, at.Ge(1)]", "1", False, "", id="Ge ok"),
        pytest.param("Annotated[int, at.Le(1)]", "2", True, "2 is not less than or equal to 1", id="Le fail"),
        pytest.param("Annotated[int, at.Le(1)]", "1", False, "", id="Le ok"),
        pytest.param("Annotated[int, at.MultipleOf(2)]", "3", True, "3 is not a multiple of 2", id="MultipleOf fail"),
        pytest.param("Annotated[int, at.MultipleOf(2)]", "4", False, "", id="MultipleOf ok"),
        pytest.param("Annotated[str, at.MinLen(2)]", "'a'", True, "a is not longer than 2", id="MinLen fail"),
        pytest.param("Annotated[str, at.MinLen(2)]", "'abc'", False, "", id="MinLen ok"),
        pytest.param("Annotated[str, at.MaxLen(2)]", "'abc'", True, "abc is not shorter than 2", id="MaxLen fail"),
        pytest.param("Annotated[str, at.MaxLen(2)]", "'a'", False, "", id="MaxLen ok"),
        pytest.param(
            "Annotated[int, at.Predicate(lambda x: False)]", "0", True, "0 does not satisfy", id="Predicate fail"
        ),
        pytest.param("Annotated[int, at.Predicate(lambda x: True)]", "0", False, "", id="Predicate ok"),
    ],
)
async def test_test_with_annotated_type(annotation: str, value: str, raises: bool, match: str):
    loc: dict = {}
    exec(
        dedent(
            f"""
            from typing import Annotated
            import annotated_types as at

            @Test("test")
            async def test(test: TestInstance, foo: {annotation}) -> None:
                pass

            test_instance = test.use_options(foo={value})
        """
        ),
        globals(),
        loc,
    )
    test = loc["test_instance"]

    ctx: AbstractContextManager
    if raises:
        ctx = pytest.raises(ValueError, match=match)
    else:
        ctx = nullcontext()

    with ctx:
        await parameterize(test, Environment(TestConfig("test")))

However, this now fails in python 3.14+

A "minimal" reproduction:

import inspect

from textwrap import dedent
from typing import TypeAlias, Callable, Coroutine, Self, get_type_hints

TestFunc: TypeAlias = Callable[..., Coroutine[None, None, None]]

class Test:
    def __call__(self, func: TestFunc) -> Self:
        print(inspect.signature(func).parameters)
        print(get_type_hints(func))

def main():
    exec(
        dedent(
            f"""
            from typing import Annotated
            import annotated_types as at

            @Test()
            async def test(foo: Annotated[int, at.Gt(1)]) -> None:
                pass

            print("After function call")
        """
        ),
        globals(),
        {},
    )


if __name__ == "__main__":
    main()

Despite the types Annotated and at being imported in the exec'd code, the values do not get resolved when trying to get annotations.

Traceback (most recent call last):
  File "/home/vfazio/development/xtf/314annotations.py", line 33, in <module>
    main()
    ~~~~^^
  File "/home/vfazio/development/xtf/314annotations.py", line 14, in main
    exec(
    ~~~~^
        dedent(
        ^^^^^^^
    ...<12 lines>...
        {},
        ^^^
    )
    ^
  File "<string>", line 5, in <module>
  File "/home/vfazio/development/xtf/314annotations.py", line 10, in __call__
    print(inspect.signature(func).parameters)
          ~~~~~~~~~~~~~~~~~^^^^^^
  File "/home/vfazio/.pyenv/versions/3.14.3/lib/python3.14/inspect.py", line 3322, in signature
    return Signature.from_callable(obj, follow_wrapped=follow_wrapped,
           ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                                   globals=globals, locals=locals, eval_str=eval_str,
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                                   annotation_format=annotation_format)
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vfazio/.pyenv/versions/3.14.3/lib/python3.14/inspect.py", line 3037, in from_callable
    return _signature_from_callable(obj, sigcls=cls,
                                    follow_wrapper_chains=follow_wrapped,
                                    globals=globals, locals=locals, eval_str=eval_str,
                                    annotation_format=annotation_format)
  File "/home/vfazio/.pyenv/versions/3.14.3/lib/python3.14/inspect.py", line 2512, in _signature_from_callable
    return _signature_from_function(sigcls, obj,
                                    skip_bound_arg=skip_bound_arg,
                                    globals=globals, locals=locals, eval_str=eval_str,
                                    annotation_format=annotation_format)
  File "/home/vfazio/.pyenv/versions/3.14.3/lib/python3.14/inspect.py", line 2335, in _signature_from_function
    annotations = get_annotations(func, globals=globals, locals=locals, eval_str=eval_str,
                                  format=annotation_format)
  File "/home/vfazio/.pyenv/versions/3.14.3/lib/python3.14/annotationlib.py", line 966, in get_annotations
    ann = _get_dunder_annotations(obj)
  File "/home/vfazio/.pyenv/versions/3.14.3/lib/python3.14/annotationlib.py", line 1146, in _get_dunder_annotations
    ann = getattr(obj, "__annotations__", None)
  File "<string>", line 6, in __annotate__
NameError: name 'Annotated' is not defined

This seems to imply that code generators/exec can no longer rely on imports in code to satisfy annotations? I don't know if this is due to changes in exec or PEP 649/749 but it is inconvenient.

CPython versions tested on:

3.14, 3.13, 3.12, 3.11

Operating systems tested on:

Linux

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.14bugs and security fixes3.15new features, bugs and security fixesstdlibStandard 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