PEP 749: Implementing PEP 649

I made a few changes to the PEP recently and I’m now at the core sprint motivated to get this PEP closer to being finished.

I ended up with a different approach on metaclasses, outlined in PEP 749: Add section on metaclasses by JelleZijlstra · Pull Request #3847 · python/peps · GitHub, that avoids some of the complexities in the previous solutions by always using the descriptors on type.

I also just posted a proposed change, PEP 749: Add a FAKE_GLOBALS_VALUE format by JelleZijlstra · Pull Request #3991 · python/peps · GitHub (based on a suggestion by @larry). It adds a new annotations format, FAKE_GLOBALS_VALUE, for internal use.

1 Like

I added a naming question on the new PR, but the basic idea sounds good.

I also think the metaclass resolution is a good compromise that respects the notion of “if the implementation is hard to explain, it’s a bad idea”.

1 Like

I am getting ready to submit this PEP to the Steering Council now. I haven’t had a lot of time to work on it recently, but the implementation has improved thanks to feedback from @larry, @carljm, and others, and thanks to @sobolevn’s valiant efforts to use the new functionality in other parts of the standard library.

Here are a few points that came up, either as issues or in a separate discussion with Larry. I don’t think these need to block the PEP, but I’d welcome any feedback on any of these:

I now aim to submit the PEP to the SC later this week.

15 Likes

What’s the status of implementing PEP 649 in general? Our project uses annotations at runtime heavily and I was happy that our extensive acceptance tests passed with Python 3.14 alpha 5. Are there big changes still coming in?

The PEP looked great for me.

1 Like

It’s implemented relatively fully on main. Changes might still be coming, though: conditional blocks in class definitions seem to be evaluating types even when these conditionals are false · Issue #130881 · python/cpython · GitHub

4 Likes

Thanks for the update and congrats for the great work! I’ll keep testing the preview releases and will submit bug reports if I encounter regressions. The corner case with conditional assignments doesn’t affect us, because we only inspect function annotations.

1 Like

Submitted to the SC: PEP 749 -- Implementing PEP 649 · Issue #282 · python/steering-council · GitHub

6 Likes

Here’s another edge case I’d like to have feedback on: partially executed modules.

This is quite a convoluted case so bear with me:

# recmod/__main__.py
from . import a
print("in __main__:", a.__annotations__)

# recmod/a.py
v1: int
from . import b
v2: int

# recmod/b.py
from . import a
print("in b:", a.__annotations__)

On Python 3.13, this does:

$ python3.13 -m recmod
in b: {'v1': <class 'int'>}
in __main__: {'v1': <class 'int'>, 'v2': <class 'int'>}   

But on current main it prints an empty dictionary twice, because accessing __annotations__ while a is still executing poisons the cache forever. I am proposing to make it instead throw an error if __annotations__ are accessed before the module is done executing.

PR here: gh-130907: Error when accessing __annotations__ on a partially defined module by JelleZijlstra · Pull Request #131550 · python/cpython · GitHub

6 Likes

What about the __main__ script or module being executed by the interpreter? In that case it’ll never be fully executed before the interpreter starts shutting down. Would it make more sense to restrict to after the last annotation is reached in any given module?

The python 3.13 behavior looks correct to me. I’m honestly quite uncomfortable with how much the behavior of 649 annotations keeps shifting further and further, as it feels like with each behavioral change tacked on, there’s likely something we aren’t going to see in advance that will be broken, and that the behavioral differences might make it significantly more difficult for library authors that use annotations at runtime to robustly support both 3.13 and 3.14

The behavioral changes being made are in response to user feedback and consideration of edge cases. If you’d like to help make sure the system works well in practice, please test the alphas of Python 3.14 against your code.

You’re taking something that has reasonable behavior currently, and suggesting it should become an error in a single version change here. It may be an edge case, but that’s a rather drastic change to go from working with a sensible interpretation to an error in a single version with no prior warning to users. No amount of testing the alphas is going to change that, but the reality is not every piece of code will be tested on the alphas and someone may be blindsided by breaking existing behavior.

6 Likes

Is it feasible to just disable the caching while the module is still executing?

1 Like

That’s possible too, but with the current implementation it would return an empty dictionary the first time around.

It should be possible by building on top of gh-130881: Handle conditionally defined annotations by JelleZijlstra · Pull Request #130935 · python/cpython · GitHub to get something close to the 3.13 behavior back. We’d have to add to __conditional_annotations__ for all annotations in the module scope, not just conditional ones, and register an __annotate__ function at the beginning of module evaluation. We’d also want to disable caching of __annotations__ while the module is partially executing.

I will likely not have time to work on that soon, but I’d encourage others to get involved.

2 Likes

I like raising an error in this case. It’s something that we could revisit and change if a need is found. Disabling the caching while the module is still executing on the other hand is not something we’d easily be able to change in the future.

2 Likes

Regardless of me liking the error… It could be “oops, too late” for us to make that change. It sounds like a change that’d be disruptive to existing code accessing __annotations__ before a module being imported has finished importing.

I don’t have a feel for “why would anyone even do that”, except that in the world of annotations and decorators: I expect it does happen.

2 Likes

Is there any work planned to improve the import time of annotationlib?

I’m bringing this up partly due to the discussion around PEP-781 as with the current implementation in 3.14.0a6 the import time of typing has increased significantly.

Comparing start times of 3.13 and 3.14 with and without typing imports:

Benchmark 1: ~/.pyenv/versions/3.14.0a6/bin/python -c ''
  Time (mean ± σ):      17.7 ms ±   4.5 ms    [User: 13.3 ms, System: 4.3 ms]
  Range (min … max):    12.8 ms …  29.3 ms    50 runs
 
Benchmark 2: ~/.pyenv/versions/3.13.2/bin/python -c ''
  Time (mean ± σ):      18.1 ms ±   4.7 ms    [User: 14.0 ms, System: 4.0 ms]
  Range (min … max):    12.6 ms …  28.3 ms    50 runs
 
Benchmark 3: ~/.pyenv/versions/3.14.0a6/bin/python -c 'import typing'
  Time (mean ± σ):      35.4 ms ±   5.6 ms    [User: 29.5 ms, System: 5.9 ms]
  Range (min … max):    27.0 ms …  47.8 ms    50 runs
 
Benchmark 4: ~/.pyenv/versions/3.13.2/bin/python -c 'import typing'
  Time (mean ± σ):      25.3 ms ±   4.7 ms    [User: 20.9 ms, System: 4.4 ms]
  Range (min … max):    19.2 ms …  39.2 ms    50 runs
 
Summary
  ~/.pyenv/versions/3.14.0a6/bin/python -c '' ran
    1.02 ± 0.37 times faster than ~/.pyenv/versions/3.13.2/bin/python -c ''
    1.43 ± 0.45 times faster than ~/.pyenv/versions/3.13.2/bin/python -c 'import typing'
    2.00 ± 0.59 times faster than ~/.pyenv/versions/3.14.0a6/bin/python -c 'import typing'

This largest chunk of this seems to be due to the usage of ast which is mostly required for the _Stringifier class. As this class isn’t always going to be needed it would be nice if it was possible to avoid this.

My initial thought would be that you could split annotationlib.py into a package with annotationlib/__init__.py and annotationlib/_stringifier.py, move the definition of _Stringifier into the latter file and defer the import of this until the class is needed. The one remaining use of ast in ForwardRef.__forward_arg__ could then also use a deferred import. I don’t know if you already have another solution in mind.

2 Likes

gh-118761: Optimise import time for ast by AA-Turner · Pull Request #131953 · python/cpython · GitHub should help. I’d be open to other optimizations too; feel free to send a PR!

Another costly piece is the enum import, which is needed to make Format into an enum. Unfortunately, that seems hard to avoid.

Perhaps we can also defer the import of annotationlib in typing. It’s needed for TypedDict, NamedTuple, and a few other things, but many users of typing don’t need those.

I’m going to have limited time in the near future (my newborn takes precedence!), but I’d be happy to review any PRs.

1 Like

Ah, I hadn’t seen the PR for ast, that will probably make a significant chunk of the difference - would need to see if it still seems worthwhile after that lands.

Yes the enum hit is mostly unavoidable as the enum is needed for the main functions.

Another import that could be deferred in annotationlib would be functools as it’s only used for one check in unwrapping.

functools is imported by enum, so it wouldn’t make a difference.