Writing an __annotate__ function to handle annotations for generated objects[1] in 3.14+ is significantly more complicated to implement than creating a dictionary for __annotations__ in earlier versions.
To simplify creating these functions, add a DEFERRED format for annotations that can be used to generate correct and complete __annotate__ functions from a previously gathered dictionary of these deferred annotations.
Background
In 3.13 and earlier it is possible to gather annotations into a dictionary and assign that dictionary to the __annotations__ attribute of a generated method in order to support runtime annotations for the method. For example this is what attrs does for its generated __init__ functions. dataclasses similarly gathers annotations when the decorator is called but uses them slightly differently and has its own issues.
Currently in Python 3.14 with the new annotations implementation when you retrieve annotations using annotationlib.get_annotations or annotationlib.call_annotate_function you can choose from three evaluated or partially evaluated formats.
With Vector = list[float]
Format.VALUEVectorbecomeslist[float]list[unknown]would cause the function to raise aNameError
Format.FORWARDREFVectorbecomeslist[float]list[unknown]becomeslist[ForwardRef('unknown', ...)]
Format.STRINGVectorbecomes"Vector"list[unknown]becomes"list[unknown]"
While this may be all that is needed for many use cases, none of these are sufficient on their own to create all of the other formats in order to synthesize a correct and complete __annotate__ function for a generated method.
The current proposed solution I have for dataclasses in 3.14 requires essentially reimplementing the dataclasses field gathering logic, which seems excessive for annotations and may fail VALUE annotations in cases where it could otherwise succeed (if thereās a forward reference in an init=False field for example).
Previous attempts tried to use the FORWARDREF format that dataclasses currently gathers in the type attributes of fields but as can be seen, those both lose names like Vector here and ForwardRef objects can be arbitrarily contained in other objects making them difficult to remove at a later point for VALUE annotations.
Proposal
Add a fourth[2] format for annotations Format.DEFERRED.
The requirement of this format is that all annotations returned must have an evaluate(format=...) method that can be used to retrieve all of the other annotation formats. For the core case, this would be the ForwardRef class, which already has this method.
So for the earlier example, this would be the result:
Format.DEFERREDVectorbecomesForwardRef('Vector', ...)list[unknown]becomesForwardRef('list[unknown]')
As ForwardRef has an .evaluate(format=...) method, these can then be evaluated into any of the existing formats.
This makes it possible to gather annotations at the time of class decoration and use them to generate an annotate function that properly supports every annotation format.
Required Changes
All of these changes would be to annotationlib. A backport package could be made to support 3.14 if needed as long as the enum value is agreed upon. I donāt believe this requires any changes to the __annotate__ functions generated by CPython itself.
- The
Formatenum gains a new valueDEFERRED- This is needed so that user generated
__annotate__functions can also return this format
- This is needed so that user generated
call_annotate_functionandget_annotationswould need to be updated for this format- The logic for this already exists to support the
FORWARDREFformat in certain cases
- The logic for this already exists to support the
ForwardRef.evaluatecan support this format by returning the object itself- A helper to create objects with an
evaluatemethod would be needed for cases where annotations have already been evaluated in some way- For example if one of the annotations sources used
__future__string annotations or if other extra values need to be added to the annotations such as the returnNonefor dataclassesā__init__
- For example if one of the annotations sources used
- A
make_annotate_functionhelper function to convert these gathered deferred references into a new__annotate__function which can be attached to the object that needs to be annotated.
make_annotate_function would be something like this:
def make_annotate_function(annotations):
# pre-processing logic to make sure all annotations support `.evaluate()`
...
def __annotate__(format, /):
match format:
case Format.VALUE | Format.FORWARDREF | Format.STRING:
return {k: v.evaluate(format) for k, v in annotations.items()}
case Format.DEFERRED:
return annotations.copy()
case _:
raise NotImplementedError(format)
return __annotate__
There will certainly be some fine details that still need to be worked out.