You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Support 3.14 annotations in converter functions #1470
While #1451 added support for forward references in the classes themselves, forward references in converter functions still fail due to the use of inspect.signature which by default uses Format.VALUE when getting annotations.
After talking to @Tinche, this seems to be a lot more complicated than it looks like.
We actually do use the results on class creation, so it might be that we'll have to special-case this entire thing and implement an __annotate__ on 3.14.
You already kind of need to generate __annotate__ for 3.14 - at least for the __init__ function - but it's difficult with the current tools available from annotationlib. String annotations from attrs currently reveal the ForwardRef values.
fromattrsimportdefinefromannotationlibimportget_annotations, FormatVector=list[float]
@defineclassExample:
examples: list[Example]
vectors: list[Vector]
# Format.STRING names show up in help()print(get_annotations(Example.__init__, format=Format.STRING))
{
'return': 'None',
# should be "list[Example]"'examples': "list[ForwardRef('Example', is_class=True, owner=<class 'ref_demo.Example'>)]",
# should be "list[Vector]"'vectors': 'list[list[float]]',
}
dataclasses has a related but different issue as it writes the types into the source code instead of generating __annotations__. See: python/cpython#137530
In trying to solve this for dataclasses I've run into the issue that you can't easily use annotations collected in construction to generate a new __annotate__ function and recreating the logic used to gather the annotations in the first place is both awkward and problematic for VALUE annotations - do you leak forward references or do you fail for annotations on fields where init=False if they contain a forward reference?
I'd like a Format for annotations that deferred the evaluation further. Internally annotationlib already uses such a format when there's an exception in trying to get FORWARDREF annotations see here.
I think having these could make generating __annotate__ functions much closer to how you can currently create the __annotations__ dict. I've been kicking around a proposal for such a feature but I wasn't sure how broadly useful it would be - would you find this helpful?
A short summary of the proposal as it's a bit long and detailed:
Annotations come in three formats. None are suitable for creating new __annotate__ functions. Add a new DEFERRED format that can be evaluated to all possible formats individually, along with a helper to convert annotations in this format into new __annotate__ callables.
I've also released a reannotate package that implements this functionality with some small modifications outside of the stdlib, I highly doubt you'd want such a dependency but it may be of interest.
dataclasses itself went with reconstructing the field gathering logic for now, though I think this is overly complicated and still fails in cases where the DEFERRED approach would succeed.
Yeah a dependency is a no go for something rather obscure like this, but if you would be game, I'd be open to transplanting the parts that we need, if that would help? I've already forgotten all the problems and intricacies in play here, so I can't speak any smarter about it. 😅
Looking at the empty dependencies that's what I suspected 🙂.
It's possible to make a reduced version of reannotate built by constructing ForwardRef instances from strings instead of evaluating the strings/ast objects directly.
I ran some performance tests and sadly this simplified version is significantly slower, especially if the annotations have lots of generics. This is because the minimised version includes additional time spent unparsing ast objects to create STRING annotations - the reannotate version stores the ast objects directly and skips this step.
It's worth noting that STRING annotations are usually slower than FORWARDREF annotations. Building deferred annotations relies on the same logic so it's also slower. VALUE annotations if they succeed are much faster again.
So there is the question of, how much of a slowdown from annotations would still be acceptable?
Performance Values
Getting annotations in the specified format 10k times each, "Evaluated Deferred" means further getting the FORWARDREF values from deferred annotations (as you'd need to analyse the annotations for processing).
These use call_annotate_function for VALUE annotations to avoid the __annotations__ cache that gets created after first retrieving them.
Format.FORWARDREF is much slower than Format.VALUE, deferred annotations no longer have the cache from the fake globals check so are also slower, but better relatively.
Activity
hynek commentedon Sep 10, 2025
After talking to @Tinche, this seems to be a lot more complicated than it looks like.
We actually do use the results on class creation, so it might be that we'll have to special-case this entire thing and implement an
__annotate__on 3.14.DavidCEllis commentedon Sep 10, 2025
You already kind of need to generate
__annotate__for 3.14 - at least for the__init__function - but it's difficult with the current tools available fromannotationlib. String annotations fromattrscurrently reveal theForwardRefvalues.{ 'return': 'None', # should be "list[Example]" 'examples': "list[ForwardRef('Example', is_class=True, owner=<class 'ref_demo.Example'>)]", # should be "list[Vector]" 'vectors': 'list[list[float]]', }dataclasseshas a related but different issue as it writes the types into the source code instead of generating__annotations__. See: python/cpython#137530In trying to solve this for dataclasses I've run into the issue that you can't easily use annotations collected in construction to generate a new
__annotate__function and recreating the logic used to gather the annotations in the first place is both awkward and problematic forVALUEannotations - do you leak forward references or do you fail for annotations on fields whereinit=Falseif they contain a forward reference?I'd like a
Formatfor annotations that deferred the evaluation further. Internallyannotationlibalready uses such a format when there's an exception in trying to getFORWARDREFannotations see here.I think having these could make generating
__annotate__functions much closer to how you can currently create the__annotations__dict. I've been kicking around a proposal for such a feature but I wasn't sure how broadly useful it would be - would you find this helpful?DavidCEllis commentedon Mar 18, 2026
A slight update on this, I made a DPO thread proposing a way to make handling 3.14+ annotations easier but didn't see much interest: https://discuss.python.org/t/add-a-format-deferred-option-for-pep-649-749-annotations/104001/2
A short summary of the proposal as it's a bit long and detailed:
Annotations come in three formats. None are suitable for creating new
__annotate__functions. Add a newDEFERREDformat that can be evaluated to all possible formats individually, along with a helper to convert annotations in this format into new__annotate__callables.I've also released a reannotate package that implements this functionality with some small modifications outside of the stdlib, I highly doubt you'd want such a dependency but it may be of interest.
dataclassesitself went with reconstructing the field gathering logic for now, though I think this is overly complicated and still fails in cases where theDEFERREDapproach would succeed.hynek commentedon Mar 18, 2026
Yeah a dependency is a no go for something rather obscure like this, but if you would be game, I'd be open to transplanting the parts that we need, if that would help? I've already forgotten all the problems and intricacies in play here, so I can't speak any smarter about it. 😅
DavidCEllis commentedon Mar 18, 2026
Looking at the empty dependencies that's what I suspected 🙂.
It's possible to make a reduced version of
reannotatebuilt by constructingForwardRefinstances from strings instead of evaluating the strings/ast objects directly.Something like this:
https://github.com/DavidCEllis/Reannotate/blob/mini-reannotate/src/reannotate/_mini_reannotate.py
Honestly I might be tempted to reduce the main version down to this, but I need to check performance and corner cases.
DavidCEllis commentedon Mar 19, 2026
No simplification for me 🙁.
I ran some performance tests and sadly this simplified version is significantly slower, especially if the annotations have lots of generics. This is because the minimised version includes additional time spent unparsing ast objects to create
STRINGannotations - thereannotateversion stores the ast objects directly and skips this step.It's worth noting that
STRINGannotations are usually slower thanFORWARDREFannotations. Building deferred annotations relies on the same logic so it's also slower.VALUEannotations if they succeed are much faster again.So there is the question of, how much of a slowdown from annotations would still be acceptable?
Performance Values
Getting annotations in the specified format 10k times each, "Evaluated Deferred" means further getting the
FORWARDREFvalues from deferred annotations (as you'd need to analyse the annotations for processing).These use
call_annotate_functionforVALUEannotations to avoid the__annotations__cache that gets created after first retrieving them.Example 1 - Basic identifiers:
Example 2 - Generics and Unions:
Example 3 - Generics and Forward References:
Format.FORWARDREFis much slower thanFormat.VALUE, deferred annotations no longer have the cache from the fake globals check so are also slower, but better relatively.Example 4 - Generics and Forward References and a missing attribute:
In this case
Format.FORWARDREFis now doing almost the same thing as deferred annotations, so they are similar in performance.