python / python/cpython

Type annotations in `exec`d code do not resolve as expected

Open
#145,426 7 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

3.14 3.15 stdlib topic-typing type-bug
Dominant language
Python
Stars
77.2k
Forks
35.9k
PR merge metrics
PR metrics pending

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by running the minimal reproduction on Python 3.14 and trace inspect.signature through annotationlib.get_annotations, especially the annotate path shown in the traceback. Compare the behavior with Python 3.13 and the existing exec-based test; done means annotations imported inside exec'd code resolve correctly without breaking the reported test scenarios.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.