Movatterモバイル変換


[0]ホーム

URL:


Following system colour schemeSelected dark colour schemeSelected light colour scheme

Python Enhancement Proposals

PEP 649 – Deferred Evaluation Of Annotations Using Descriptors

Author:
Larry Hastings <larry at hastings.org>
Discussions-To:
Discourse thread
Status:
Final
Type:
Standards Track
Topic:
Typing
Created:
11-Jan-2021
Python-Version:
3.14
Post-History:
11-Jan-2021,12-Apr-2021,18-Apr-2021,09-Aug-2021,20-Oct-2021,20-Oct-2021,17-Nov-2021,15-Mar-2022,23-Nov-2022,07-Feb-2023,11-Apr-2023
Replaces:
563
Resolution:
08-May-2023

Table of Contents

Important

This PEP is a historical document. The up-to-date, canonical documentation can now be found atAnnotations.

×

SeePEP 1 for how to propose changes.

Abstract

Annotations are a Python technology that allows expressingtype information and other metadata about Python functions,classes, and modules. But Python’s original semanticsfor annotations required them to be eagerly evaluated,at the time the annotated object was bound. This causedchronic problems for static type analysis users using“type hints”, due to forward-reference and circular-referenceproblems.

Python solved this by acceptingPEP 563, incorporatinga new approach called “stringized annotations” in whichannotations were automatically converted into strings byPython. This solved the forward-reference and circular-referenceproblems, and also fostered intriguing new uses for annotationmetadata. But stringized annotations in turn caused chronicproblems for runtime users of annotations.

This PEP proposes a new and comprehensive third approachfor representing and computing annotations. It adds a newinternal mechanism for lazily computing annotations on demand,via a new object method called__annotate__.This approach, when combined with a novel technique forcoercing annotation values into alternative formats, solvesall the above problems, supports all existing use cases,and should foster future innovations in annotations.

Overview

This PEP adds a new dunder attribute to the objects thatsupport annotations–functions, classes, and modules.The new attribute is called__annotate__, and isa reference to a function which computes and returnsthat object’s annotations dict.

At compile time, if the definition of an object includesannotations, the Python compiler will write the expressionscomputing the annotations into its own function. When run,the function will return the annotations dict. The Pythoncompiler then stores a reference to this function in__annotate__ on the object.

Furthermore,__annotations__ is redefined to be a“data descriptor” which calls this annotation function onceand caches the result.

This mechanism delays the evaluation of annotations expressionsuntil the annotations are examined, which solves many circularreference problems.

This PEP also defines new functionality for two functionsin the Python standard library:inspect.get_annotations andtyping.get_type_hints.The functionality is accessed via a new keyword-only parameter,format.format allows the user to requestthe annotations from these functionsin a specific format.Format identifiers are always predefined integer values.The formats defined by this PEP are:

  • inspect.VALUE=1

    The default value.The function will return the conventional Pythonvalues for the annotations. This format is identicalto the return value for these functions under Python 3.11.

  • inspect.FORWARDREF=2

    The function will attempt to return the conventionalPython values for the annotations. However, if itencounters an undefined name, or a free variable thathas not yet been associated with a value, it dynamicallycreates a proxy object (aForwardRef) that substitutesfor that value in the expression, then continues evaluation.The resulting dict may contain a mixture of proxies andreal values. If all real values are defined at the timethe function is called,inspect.FORWARDREF andinspect.VALUE produce identical results.

  • inspect.SOURCE=3

    The function will produce an annotation dictionarywhere the values have been replaced by strings containingthe original source code for the annotation expressions.These strings may only be approximate, as they may bereverse-engineered from another format, rather thanpreserving the original source code, but the differenceswill be minor.

If accepted, this PEP wouldsupersedePEP 563,andPEP 563’s behavior would be deprecated andeventually removed.

Comparison Of Annotation Semantics

Note

The code presented in this section is simplifiedfor clarity, and is intentionally inaccurate in somecritical aspects. This example is intended merely tocommunicate the high-level concepts involved withoutgetting lost in the details. But readers should notethat the actual implementation is quite different inseveral important ways. See theImplementationsection later in this PEP for a far more accuratedescription of what this PEP proposes from a technicallevel.

Consider this example code:

deffoo(x:int=3,y:MyType=None)->float:...classMyType:...foo_y_annotation=foo.__annotations__['y']

As we see here, annotations are available at runtime through an__annotations__ attribute on functions, classes, and modules.When annotations are specified on one of these objects,__annotations__ is a dictionary mapping the names of thefields to the value specified as that field’s annotation.

The default behavior in Python is to evaluate the expressionsfor the annotations, and build the annotations dict, at the timethe function, class, or module is bound. At runtime the abovecode actually works something like this:

annotations={'x':int,'y':MyType,'return':float}deffoo(x=3,y="abc"):...foo.__annotations__=annotationsclassMyType:...foo_y_annotation=foo.__annotations__['y']

The crucial detail here is that the valuesint,MyType,andfloat are looked up at the time the function object isbound, and these values are stored in the annotations dict.But this code doesn’t run—it throws aNameError on the firstline, becauseMyType hasn’t been defined yet.

PEP 563’s solution is to decompile the expressions backinto strings during compilation and store those strings as thevalues in the annotations dict. The equivalent runtime codewould look something like this:

annotations={'x':'int','y':'MyType','return':'float'}deffoo(x=3,y="abc"):...foo.__annotations__=annotationsclassMyType:...foo_y_annotation=foo.__annotations__['y']

This code now runs successfully. However,foo_y_annotationis no longer a reference toMyType, it is thestring'MyType'. To turn the string into the real valueMyType,the user would need to evaluate the string usingeval,inspect.get_annotations, ortyping.get_type_hints.

This PEP proposes a third approach, delaying the evaluation ofthe annotations by computing them in their own function. Ifthis PEP was active, the generated code would work somethinglike this:

classfunction:# __annotations__ on a function object is already a# "data descriptor" in Python, we're just changing# what it does@propertydef__annotations__(self):returnself.__annotate__()# ...defannotate_foo():return{'x':int,'y':MyType,'return':float}deffoo(x=3,y="abc"):...foo.__annotate__=annotate_fooclassMyType:...foo_y_annotation=foo.__annotations__['y']

The important change is that the code constructing theannotations dict now lives in a function—here, calledannotate_foo(). But this function isn’t calleduntil we ask for the value offoo.__annotations__,and we don’t do that untilafter the definition ofMyType.So this code also runs successfully, andfoo_y_annotation nowhas the correct value–the classMyType–even thoughMyType wasn’t defined untilafter the annotation wasdefined.

Mistaken Rejection Of This Approach In November 2017

During the early days of discussion aroundPEP 563,in a November 2017 thread incomp.lang.python-dev,the idea of using code to delay the evaluation ofannotations was briefly discussed. At the time thetechnique was termed an “implicit lambda expression”.

Guido van Rossum—Python’s BDFL at the time—replied,asserting that these “implicit lambda expression” wouldn’twork, because they’d only be able to resolve symbols atmodule-level scope:

IMO the inability of referencing class-level definitionsfrom annotations on methods pretty much kills this idea.

https://mail.python.org/pipermail/python-dev/2017-November/150109.html

This led to a short discussion about extending lambda-izedannotations for methods to be able to refer to class-leveldefinitions, by maintaining a reference to the class-levelscope. This idea, too, was quickly rejected.

PEP 563 summarizes the above discussion

The approach taken by this PEP doesn’t suffer from theserestrictions. Annotations can access module-level definitions,class-level definitions, and even local and free variables.

Motivation

A History Of Annotations

Python 3.0 shipped with a new syntax feature, “annotations”,defined inPEP 3107.This allowed specifying a Python value that would beassociated with a parameter of a Python function, orwith the value that function returns.Said another way, annotations gave Python users an interfaceto provide rich metadata about a function parameter or returnvalue, for example type information.All the annotations for a function were stored together ina new attribute__annotations__, in an “annotation dict”that mapped parameter names (or, in the case of the returnannotation, using the name'return') to their Python value.

In an effort to foster experimentation, Pythonintentionally didn’t define what form this metadata should take,or what values should be used. User code began experimenting withthis new facility almost immediately. But popular libraries thatmake use of this functionality were slow to emerge.

After years of little progress, the BDFL chose a particularapproach for expressing static type information, calledtype hints, as defined inPEP 484. Python 3.5 shippedwith a newtyping module which quickly became very popular.

Python 3.6 added syntax to annotate local variables,class attributes, and module attributes, using the approachproposed inPEP 526. Static type analysis continued togrow in popularity.

However, static type analysis users were increasingly frustratedby an inconvenient problem: forward references. In classicPython, if a class C depends on a later-defined class D,it’s normally not a problem, because user code will usuallywait until both are defined before trying to use either.But annotations added a new complication, because they werecomputed at the time the annotated object (function, class,or module) was bound. If methods on class C are annotated withtype D, and these annotation expressions are computed at thetime that the method is bound, D may not be defined yet.And if methods in D are also annotated with type C, you nowhave an unresolvable circular reference problem.

Initially, static type users worked around this problemby defining their problematic annotations as strings.This worked because a string containing the type hint wasjust as usable for the static type analysis tool.And users of static type analysis tools rarely examine theannotations at runtime, so this representation wasn’titself an inconvenience. But manually stringizing typehints was clumsy and error-prone. Also, code bases wereadding more and more annotations, which consumed more andmore CPU time to create and bind.

To solve these problems, the BDFL acceptedPEP 563, whichadded a new feature to Python 3.7: “stringized annotations”.It was activated with a future import:

from__future__importannotations

Normally, annotation expressions were evaluated at the timethe object was bound, with their values being stored in theannotations dict. When stringized annotations were active,these semantics changed: instead, at compile time, the compilerconverted all annotations in that module into stringrepresentations of their source code–thus,automaticallyturning the users’s annotations into strings, obviating theneed tomanually stringize them as before.PEP 563suggested users could evaluate this string withevalif the actual value was needed at runtime.

(From here on out, this PEP will refer to the classicsemantics ofPEP 3107 andPEP 526, where thevalues of annotation expressions are computed at the timethe object is bound, as“stock” semantics, to differentiatethem from the newPEP 563 “stringized” annotation semantics.)

The Current State Of Annotation Use Cases

Although there are many specific use cases for annotations,annotation users in the discussion around this PEP tendedto fall into one of these four categories.

Static typing users

Static typing users use annotations to add type informationto their code. But they largely don’t examine the annotationsat runtime. Instead, they use static type analysis tools(mypy, pytype) to examine their source tree and determinewhether or not their code is using types consistently.This is almost certainly the most popular use case forannotations today.

Many of the annotations usetype hints, a laPEP 484(and many subsequent PEPs). Type hints are passive objects,mere representation of type information; they don’t do any actual work.Type hints are often parameterized with other types or other type hints.Since they’re agnostic about what these actual values are, type hintswork fine withForwardRef proxy objects.Users of static type hints discovered that extensive type hinting understock semantics often created large-scale circular reference and circularimport problems that could be difficult to solve.PEP 563 was designedspecifically to solve this problem, and the solution worked great forthese users. The difficulty of rendering stringized annotations intoreal values largely didn’t inconvenience these users because of howinfrequently they examine annotations at runtime.

Static typing users often combinePEP 563 with theiftyping.TYPE_CHECKING idiom to prevent their type hints from beingloaded at runtime. This means they often aren’t able to evaluate theirstringized annotations and produce real values at runtime. On the rareoccasion that they do examine annotations at runtime, they often forgoeval, instead using lexical analysis directly on the stringizedannotations.

Under this PEP, static typing users will probably preferFORWARDREForSOURCE format.

Runtime annotation users

Runtime annotation users use annotations as a means of expressing richmetadata about their functions and classes, which they use as input toruntime behavior. Specific use cases include runtime type verification(Pydantic) and glue logic to expose Python APIs in another domain(FastAPI, Typer). The annotations may or may not be type hints.

As runtime annotation users examine annotations at runtime, they weretraditionally better served with stock semantics. This use case islargely incompatible withPEP 563, particularly with theiftyping.TYPE_CHECKING idiom.

Under this PEP, runtime annotation users will most likely preferVALUEformat, though some (e.g. if they evaluate annotations eagerly in a decoratorand want to support forward references) may also useFORWARDREF format.

Wrappers

Wrappers are functions or classes that wrap user functions orclasses and add functionality. Examples of this would bedataclass(),functools.partial(),attrs, andwrapt.

Wrappers are a distinct subcategory of runtime annotation users.Although they do use annotations at runtime, they may or may notactually examine the annotations of the objects they wrap–it dependson the functionality the wrapper provides. As a rule they shouldpropagate the annotations of the wrapped object to the wrapperthey create, although it’s possible they may modify those annotations.

Wrappers were generally designed to work well under stock semantics.Whether or not they work well underPEP 563 semantics depends on thedegree to which they examine the wrapped object’s annotations.Often wrappers don’t care about the value per se, only needingspecific information about the annotations. Even so,PEP 563and theiftyping.TYPE_CHECKING idiom can make it difficultfor wrappers to reliably determine the information they need atruntime. This is an ongoing, chronic problem.Under this PEP, wrappers will probably preferFORWARDREF formatfor their internal logic. But the wrapped objects need to supportall formats for their users.

Documentation

PEP 563 stringized annotations were a boon for tools thatmechanically construct documentation.

Stringized type hints make for excellent documentation; type hintsas expressed in source code are often succinct and readable. However,at runtime these same type hints can produce value at runtime whose repris a sprawling, nested, unreadable mess. Thus documentation users werewell-served byPEP 563 but poorly served with stock semantics.

Under this PEP, documentation users are expected to useSOURCE format.

Motivation For This PEP

Python’s original semantics for annotations made its use forstatic type analysis painful due to forward reference problems.PEP 563 solved the forward reference problem, and manystatic type analysis users became happy early adopters of it.But its unconventional solution created new problems for twoof the above cited use cases: runtime annotation users,and wrappers.

First, stringized annotations didn’t permit referencing local orfree variables, which meant many useful, reasonable approachesto creating annotations were no longer viable. This wasparticularly inconvenient for decorators that wrap existingfunctions and classes, as these decorators often use closures.

Second, in order foreval to correctly look up globals in astringized annotation, you must first obtain a referenceto the correct module.But class objects don’t retain a reference to their globals.PEP 563 suggests looking up a class’s module by name insys.modules—a surprising requirement for a language-levelfeature.

Additionally, complex but legitimate constructions can make itdifficult to determine the correct globals and locals dicts togive toeval to properly evaluate a stringized annotation.Even worse, in some situations it may simply be infeasible.

For example, some libraries (e.g.typing.TypedDict,dataclasses)wrap a user class, then merge all the annotations from all thatclass’s base classes together into one cumulative annotations dict.If those annotations were stringized, callingeval on them latermay not work properly, because the globals dictionary used for theeval will be the module where theuser class was defined,which may not be the same module where theannotation wasdefined. However, if the annotations were stringized becauseof forward-reference problems, callingeval on them earlymay not work either, due to the forward reference not beingresolvable yet. This has proved to be difficult to reconcile;of the three bug reports linked to below, only one has beenmarked as fixed.

Even with proper globalsand locals,eval can be unreliableon stringized annotations.eval can only succeed if all the symbols referenced inan annotations are defined. If a stringized annotation refersto a mixture of defined and undefined symbols, a simpleevalof that string will fail. This is a problem for libraries withthat need to examine the annotation, because they can’t reliablyconvert these stringized annotations into real values.

  • Some libraries (e.g.dataclasses) solved this by foregoing realvalues and performing lexical analysis of the stringized annotation,which requires a lot of work to get right.
  • Other libraries still suffer with this problem,which can produce surprising runtime behavior.https://github.com/python/cpython/issues/97727

Also,eval() is slow, and it isn’t always available; it’ssometimes removed for space reasons on certain platforms.eval() on MicroPython doesn’t support thelocalsargument, which makes converting stringized annotationsinto real values at runtime even harder.

Finally,PEP 563 requires Python implementations tostringize their annotations. This is surprising behavior—unprecedentedfor a language-level feature, with a complicated implementation,that must be updated whenever a new operator is added to thelanguage.

These problems motivated the research into finding a newapproach to solve the problems facing annotations users,resulting in this PEP.

Implementation

Observed semantics for annotations expressions

For any objecto that supports annotations,provided that all names evaluated in the annotations expressionsare bound beforeo is defined and never subsequently rebound,o.__annotations__ will produce an identical annotations dict bothwhen “stock” semantics are active and when this PEP is active.In particular, name resolution will be performed identically inboth scenarios.

When this PEP is active, the value ofo.__annotations__won’t be calculated until the first timeo.__annotations__itself is evaluated. All evaluation of the annotation expressionsis delayed until this moment, which also means that

  • names referenced in the annotations expressions will use theircurrent value at this moment, and
  • if evaluating the annotations expressions raises an exception,that exception will be raised at this moment.

Onceo.__annotations__ is successfully calculated for thefirst time, this value is cached and will be returned by futurerequests foro.__annotations__.

__annotate__ and __annotations__

Python supports annotations on three different types:functions, classes, and modules. This PEP modifiesthe semantics on all three of these types in a similarway.

First, this PEP adds a new “dunder” attribute,__annotate__.__annotate__ must be a “data descriptor”,implementing all three actions: get, set, and delete.The__annotate__ attribute is always defined,and may only be set to eitherNone or to a callable.(__annotate__ cannot be deleted.) If an objecthas no annotations,__annotate__ should beinitialized toNone, rather than to a functionthat returns an empty dict.

The__annotate__ data descriptor must have dedicatedstorage inside the object to store the reference to its value.The location of this storage at runtime is an implementationdetail. Even if it’s visible to Python code, it should stillbe considered an internal implementation detail, and Pythoncode should prefer to interact with it only via the__annotate__ attribute.

The callable stored in__annotate__ must accept asingle required positional argument calledformat,which will always be anint (or a subclass ofint).It must either return a dict (or subclass of dict) orraiseNotImplementedError().

Here’s a formal definition of__annotate__, as it willappear in the “Magic methods” section of the PythonLanguage Reference:

__annotate__(format:int)->dict

Returns a new dictionary object mapping attribute/parameternames to their annotation values.

Takes aformat parameter specifying the format in whichannotations values should be provided. Must be one of thefollowing:

inspect.VALUE (equivalent to theint constant1)

Values are the result of evaluating the annotation expressions.

inspect.FORWARDREF (equivalent to theint constant2)

Values are real annotation values (as perinspect.VALUE format)for defined values, andForwardRef proxies for undefined values.Real objects may be exposed to, or contain references to,ForwardRef proxy objects.

inspect.SOURCE (equivalent to theint constant3)

Values are the text string of the annotation as itappears in the source code. May only be approximate;whitespace may be normalized, and constant values maybe optimized. It’s possible the exact values of thesestrings could change in future version of Python.

If an__annotate__ function doesn’t support the requestedformat, it must raiseNotImplementedError().__annotate__ functions must always support1 (inspect.VALUE)format; they must not raiseNotImplementedError() when called withformat=1.

When called withformat=1, an__annotate__ functionmay raiseNameError; it must not raiseNameError when calledrequesting any other format.

If an object doesn’t have any annotations,__annotate__ shouldpreferably be set toNone (it can’t be deleted), rather than set to afunction that returns an empty dict.

When the Python compiler compiles an object withannotations, it simultaneously compiles the appropriateannotate function. This function, called withthe single positional argumentinspect.VALUE,computes and returns the annotations dict as definedon that object. The Python compiler and runtime workin concert to ensure that the function is bound tothe appropriate namespaces:

  • For functions and classes, the globals dictionary willbe the module where the object was defined. If the objectis itself a module, its globals dictionary will be itsown dict.
  • For methods on classes, and for classes, the locals dictionarywill be the class dictionary.
  • If the annotations refer to free variables, the closure willbe the appropriate closure tuple containing cells for free variables.

Second, this PEP requires that the existing__annotations__ must be a “data descriptor”,implementing all three actions: get, set, and delete.__annotations__ must also have its own internalstorage it uses to cache a reference to the annotations dict:

  • Class and module objects mustcache the annotations dict in their__dict__, using the key__annotations__. This is required for backwardscompatibility reasons.
  • For function objects, storage for the annotations dictcache is an implementation detail. It’s preferably internalto the function object and not visible in Python.

This PEP defines semantics on how__annotations__ and__annotate__ interact, for all three types that implement them.In the following examples,fn represents a function,clsrepresents a class,mod represents a module, ando representsan object of any of these three types:

  • Wheno.__annotations__ is evaluated, and the internal storageforo.__annotations__ is unset, ando.__annotate__ is setto a callable, the getter foro.__annotations__ callso.__annotate__(1), then caches the result in its internalstorage and returns the result.
    • To explicitly clarify one question that has come up multiple times:thiso.__annotations__ cache is theonly caching mechanismdefined in this PEP. There areno other caching mechanisms definedin this PEP. The__annotate__ functions generated by the Pythoncompiler explicitly don’t cache any of the values they compute.
  • Settingo.__annotate__ to a callable invalidates thecached annotations dict.
  • Settingo.__annotate__ toNone has no effect onthe cached annotations dict.
  • Deletingo.__annotate__ raisesTypeError.__annotate__ must always be set; this prevents unannotatedsubclasses from inheriting the__annotate__ method of oneof their base classes.
  • Settingo.__annotations__ to a legal valueautomatically setso.__annotate__ toNone.
    • Settingcls.__annotations__ ormod.__annotations__toNone otherwise works like any other attribute; theattribute is set toNone.
    • Settingfn.__annotations__ toNone invalidatesthe cached annotations dict. Iffn.__annotations__doesn’t have a cached annotations value, andfn.__annotate__isNone, thefn.__annotations__ data descriptorcreates, caches, and returns a new empty dict. (This is forbackwards compatibility withPEP 3107 semantics.)

Changes to allowable annotations syntax

__annotate__ now delays the evaluation of annotations until__annotations__ is referenced in the future. It also meansannotations are evaluated in a new function, rather than in theoriginal context where the object they were defined on was bound.There are four operators with significant runtime side-effectsthat were permitted in stock semantics, but are disallowed whenfrom__future__importannotations is active, and will haveto be disallowed when this PEP is active:

  • :=
  • yield
  • yieldfrom
  • await

Changes toinspect.get_annotations andtyping.get_type_hints

(This PEP makes frequent reference to these two functions. In the futureit will refer to them collectively as “the helper functions”, as they helpuser code work with annotations.)

These two functions extract and return the annotations from an object.inspect.get_annotations returns the annotations unchanged;for the convenience of static typing users,typing.get_type_hintsmakes some modifications to the annotations before it returns them.

This PEP adds a new keyword-only parameter to these two functions,format.format specifies what format the values in theannotations dict should be returned in.Theformat parameter on these two functions accepts the same valuesas theformat parameter on the__annotate__ magic methoddefined above; however, theseformat parameters also have a defaultvalue ofinspect.VALUE.

When either__annotations__ or__annotate__ is updated on anobject, the other of those two attributes is now out-of-date and should alsoeither be updated or deleted (set toNone, in the case of__annotate__which cannot be deleted). In general, the semantics established in the previoussection ensure that this happens automatically. However, there’s one case whichfor all practical purposes can’t be handled automatically: when the dict cachedbyo.__annotations__ is itself modified, or when mutable values inside thatdict are modified.

Since this can’t be handled in code, it must be handled indocumentation. This PEP proposes amending the documentationforinspect.get_annotations (and similarly fortyping.get_type_hints) as follows:

If you directly modify the__annotations__ dict on an object,by default these changes may not be reflected in the dictionaryreturned byinspect.get_annotations when requesting eitherSOURCE orFORWARDREF format on that object. Rather thanmodifying the__annotations__ dict directly, consider replacingthat object’s__annotate__ method with a function computingthe annotations dict with your desired values. Failing that, it’sbest to overwrite the object’s__annotate__ method withNoneto preventinspect.get_annotations from generating stale resultsforSOURCE andFORWARDREF formats.

Thestringizer and thefakeglobals environment

As originally proposed, this PEP supported many runtimeannotation user use cases, and many static type user use cases.But this was insufficient–this PEP could not be accepteduntil it satisfiedall extant use cases. This becamea longtime blocker of this PEP until Carl Meyer proposedthe “stringizer” and the “fake globals” environment asdescribed below. These techniques allow this PEP to supportboth theFORWARDREF andSOURCE formats, ablysatisfying all remaining uses cases.

In a nutshell, this technique involves running aPython-compiler-generated__annotate__ function inan exotic runtime environment. Its normalglobalsdict is replaced with what’s called a “fake globals” dict.A “fake globals” dict is a dict with one important difference:every time you “get” a key from it that isn’t mapped,it creates, caches, and returns a new value for that key(as per the__missing__ callback for a dictionary).That value is a an instance of a novel type referred toas a “stringizer”.

A “stringizer” is a Python class with highly unusual behavior.Every stringizer is initialized with its “value”, initiallythe name of the missing key in the “fake globals” dict. Thestringizer then implements every Python “dunder” method used toimplement operators, and the value returned by that methodis a new stringizer whose value is a text representationof that operation.

When these stringizers are used in expressions, the resultof the expression is a new stringizer whose name textuallyrepresents that expression. For example, let’s sayyou have a variablef, which is a reference to astringizer initialized with the value'f'. Here aresome examples of operations you could perform onf andthe values they would return:

>>>fStringizer('f')>>>f+3Stringizer('f + 3')>> f["key"]Stringizer('f["key"]')

Bringing it all together: if we run a Python-generated__annotate__ function, but we replace its globalswith a “fake globals” dict, all undefined symbols itreferences will be replaced with stringizer proxy objectsrepresenting those symbols, and any operations performedon those proxies will in turn result in proxiesrepresenting that expression. This allows__annotate__to complete, and to return an annotations dict, withstringizer instances standing in for names and entireexpressions that could not have otherwise been evaluated.

In practice, the “stringizer” functionality will be implementedin theForwardRef object currently defined in thetyping module.ForwardRef will be extended toimplement all stringizer functionality; it will also beextended to support evaluating the string it contains,to produce the real value (assuming all symbols referencedare defined). This means theForwardRef objectwill retain references to the appropriate “globals”,“locals”, and even “closure” information needed toevaluate the expression.

This technique is the core of howinspect.get_annotationssupportsFORWARDREF andSOURCE formats. Initially,inspect.get_annotations will call the object’s__annotate__ method requesting the desired format.If that raisesNotImplementedError,inspect.get_annotationswill construct a “fake globals” environment, then callthe object’s__annotate__ method.

  • inspect.get_annotations producesSOURCE formatby creating a new empty “fake globals” dict, binding itto the object’s__annotate__ method, calling thatrequestingVALUE format, and then extracting the string“value” from eachForwardRef objectin the resulting dict.
  • inspect.get_annotations producesFORWARDREF formatby creating a new empty “fake globals” dict, pre-populatingit with the current contents of the__annotate__ method’sglobals dict, binding the “fake globals” dict to the object’s__annotate__ method, calling that requestingVALUEformat, and returning the result.

This entire technique works because the__annotate__ functionsgenerated by the compiler are controlled by Python itself, andare simple and predictable. They’reeffectively a singlereturn statement, computing andreturning the annotations dict. Since most operations neededto compute an annotation are implemented in Python using dundermethods, and the stringizer supports all the relevant dundermethods, this approach is a reliable, practical solution.

However, it’s not reasonable to attempt this technique withjust any__annotate__ method. This PEP assumes thatthird-party libraries may implement their own__annotate__methods, and those functions would almost certainly workincorrectly when run in this “fake globals” environment.For that reason, this PEP allocates a flag on code objects,one of the unused bits inco_flags, to mean “This codeobject can be run in a ‘fake globals’ environment.” Thismakes the “fake globals” environment strictly opt-in, andit’s expected that only__annotate__ methods generatedby the Python compiler will set it.

The weakness in this technique is in handling operators whichdon’t directly map to dunder methods on an object. These areall operators that implement some manner of flow control,either branching or iteration:

  • Short-circuitingor
  • Short-circuitingand
  • Ternary operator (theif /then operator)
  • Generator expressions
  • List / dict / set comprehensions
  • Iterable unpacking

As a rule these techniques aren’t used in annotations,so it doesn’t pose a problem in practice. However, therecent addition ofTypeVarTuple to Python does useiterable unpacking. The dunder methodsinvolved (__iter__ and__next__) don’t permitdistinguishing between iteration use cases; in order tocorrectly detect which use case was involved, mere“fake globals” and a “stringizer” wouldn’t be sufficient;this would require a custom bytecode interpreter designedspecifically around producingSOURCE andFORWARDREFformats.

Thankfully there’s a shortcut that will work fine:the stringizer will simply assume that when itsiteration dunder methods are called, it’s in serviceof iterator unpacking being performed byTypeVarTuple.It will hard-code this behavior. This means no othertechnique using iteration will work, but in practicethis won’t inconvenience real-world use cases.

Finally, note that the “fake globals” environmentwill also require constructing a matching “fake locals”dictionary, which forFORWARDREF format will bepre-populated with the relevant locals dict. The“fake globals” environment will also have to createa fake “closure”, a tuple ofForwardRef objectspre-created with the names of the free variablesreferenced by the__annotate__ method.

ForwardRef proxies created from__annotate__methods that reference free variables will map thenames and closure values of those free variables intothe locals dictionary, to ensure thateval usesthe correct values for those names.

Compiler-generated__annotate__ functions

As mentioned in the previous section, the__annotate__functions generated by the compiler are simple. They’remainly a singlereturn statement, computing andreturning the annotations dict.

However, the protocol forinspect.get_annotationsto request eitherFORWARDREF orSOURCE formatrequires first asking the__annotate__ method toproduce it.__annotate__ methods generated bythe Python compiler won’t support either of theseformats and will raiseNotImplementedError().

Third-party__annotate__ functions

Third-party classes and functions will likely needto implement their own__annotate__ methods,so that downstream users ofthose objects can take full advantage of annotations.In particular, wrappers will likely need to transformthe annotation dicts produced by the wrapped object: adding,removing, or modifying the dictionary in some way.

Most of the time, third-party code will implementtheir__annotate__ methods by callinginspect.get_annotations on some existing upstreamobject. For example, wrappers will likely request theannotations dict for their wrapped object,in the format that was requested from them, thenmodify the returned annotations dict as appropriateand return that. This allows third-party code toleverage the “fake globals” technique withouthaving to understand or participate in it.

Third-party libraries that support both pre- andpost-PEP-649 versions of Python will have to innovatetheir own best practices on how to support both.One sensible approach would be for their wrapper toalways support__annotate__, then call it requestingVALUE format and store the result as the__annotations__ on their wrapper object.This would support pre-649 Python semantics, and beforward-compatible with post-649 semantics.

Pseudocode

Here’s high-level pseudocode forinspect.get_annotations:

defget_annotations(o,format):ifformat==VALUE:returndict(o.__annotations__)ifformat==FORWARDREF:try:returndict(o.__annotations__)exceptNameError:passifnothasattr(o.__annotate__):return{}c_a=o.__annotate__try:returnc_a(format)exceptNotImplementedError:ifnotcan_be_called_with_fake_globals(c_a):return{}c_a_with_fake_globals=make_fake_globals_version(c_a,format)returnc_a_with_fake_globals(VALUE)

Here’s what a Python compiler-generated__annotate__ methodmight look like if it was written in Python:

def__annotate__(self,format):ifformat!=1:raiseNotImplementedError()return{...}

Here’s how a third-party wrapper class might implement__annotate__. In this example, the wrapper workslikefunctools.partial, pre-binding one parameter ofthe wrapped callable, which for simplicity must be namedarg:

def__annotate__(self,format):ann=inspect.get_annotations(self.wrapped_fn,format)if'arg'inann:delann['arg']returnann

Other modifications to the Python runtime

This PEP does not dictate exactly how it should beimplemented; that is left up to the language implementationmaintainers. However, the best implementation of thisPEP may require adding additional information to existingPython objects, which is implicitly condoned by the acceptanceof this PEP.

For example, it may be necessary to add a__globals__ attribute to class objects, so that the__annotate__ function for that class can be lazilybound, only on demand. Also,__annotate__ functionsdefined on methods defined in a class may need to retaina reference to the class’s__dict__, in order tocorrectly evaluate names bound in that class. It’s expectedthat the CPython implementation of this PEP will includeboth those new attributes.

All such new information added to existing Python objectsshould be done with “dunder” attributes, as they will ofcourse be implementation details.

Interactive REPL Shell

The semantics established in this PEP also hold true when executingcode in Python’s interactive REPL shell, except for module annotationsin the interactive module (__main__) itself. Since that module isnever “finished”, there’s no specific point where we can compile the__annotate__ function.

For the sake of simplicity, in this case we forego delayed evaluation.Module-level annotations in the REPL shell will continue to workexactly as they do with “stock semantics”, evaluating immediately andsetting the result directly inside the__annotations__ dict.

Annotations On Local Variables Inside Functions

Python supports syntax for local variable annotations insidefunctions. However, these annotations have no runtimeeffect–they’re discarded at compile-time. Therefore, thisPEP doesn’t need to do anything to support them, the sameas stock semantics andPEP 563.

Prototype

The original prototype implementation of this PEP can be found here:

https://github.com/larryhastings/co_annotations/

As of this writing, the implementation is severely out of date;it’s based on Python 3.10 and implements the semantics of thefirst draft of this PEP, from early 2021. It will be updatedshortly.

Performance Comparison

Performance with this PEP is generally favorable. There are fourscenarios to consider:

  • the runtime cost when annotations aren’t defined,
  • the runtime cost when annotations are defined butnot referenced, and
  • the runtime cost when annotations are defined and referenced as objects.
  • the runtime cost when annotations are defined and referenced as strings.

We’ll examine each of these scenarios in the context of all threesemantics for annotations: stock,PEP 563, and this PEP.

When there are no annotations, all three semantics have the sameruntime cost: zero. No annotations dict is created and no code isgenerated for it. This requires no runtime processor time andconsumes no memory.

When annotations are defined but not referenced, the runtime costof Python with this PEP is roughly the same asPEP 563, andimproved over stock. The specifics depend on the objectbeing annotated:

  • With stock semantics, the annotations dict is always built, andset as an attribute of the object being annotated.
  • InPEP 563 semantics, for function objects, a precompiledconstant (a specially constructed tuple) is set as an attributeof the function. For class and module objects, the annotationsdict is always built and set as an attribute of the class or module.
  • With this PEP, a single object is set as an attribute of theobject being annotated. Most of the time, this object isa constant (a code object), but when the annotations require aclass namespace or closure, this object will be a tuple constructedat binding time.

When annotations are both defined and referenced as objects, code usingthis PEP should be much faster thanPEP 563, and be as fastor faster than stock.PEP 563 semantics requires invokingeval() for every value inside an annotations dict which isenormously slow. And the implementation of this PEP generates measurablymore efficient bytecode for class and module annotations than stocksemantics; for function annotations, this PEP and stock semanticsshould be about the same speed.

The one case where this PEP will be noticeably slower thanPEP 563 is whenannotations are requested as strings; it’s hard to beat “they are alreadystrings.” But stringified annotations are intended for online documentation usecases, where performance is less likely to be a key factor.

Memory use should also be comparable in all three scenarios acrossall three semantic contexts. In the first and third scenarios,memory usage should be roughly equivalent in all cases.In the second scenario, when annotations are defined but notreferenced, using this PEP’s semantics will mean thefunction/class/module will store one unused code object (possiblybound to an unused function object); with the other two semantics,they’ll store one unused dictionary or constant tuple.

Backwards Compatibility

Backwards Compatibility With Stock Semantics

This PEP preserves nearly all existing behavior ofannotations from stock semantics:

  • The format of the annotations dict stored inthe__annotations__ attribute is unchanged.Annotations dicts contain real values, not stringsas perPEP 563.
  • Annotations dicts are mutable, and any changes to them arepreserved.
  • The__annotations__ attribute can be explicitly set,and any legal value set this way will be preserved.
  • The__annotations__ attribute can be deleted usingthedel statement.

Most code that works with stock semantics shouldcontinue to work when this PEP is active without anymodification necessary. But there are exceptions,as follows.

First, there’s a well-known idiom for accessing classannotations which may not work correctly when thisPEP is active. The original implementation of classannotations had what can only be called a bug: if a classdidn’t define any annotations of its own, but oneof its base classes did define annotations, the classwould “inherit” those annotations. This behaviorwas never desirable, so user code found a workaround:instead of accessing the annotations on the classdirectly viacls.__annotations__, code wouldaccess the class’s annotations via its dict as incls.__dict__.get("__annotations__",{}). Thisidiom worked because classes stored their annotationsin their__dict__, and accessing them this wayavoided the lookups in the base classes. The techniquerelied on implementation details of CPython, so itwas never supported behavior–though it was necessary.However, when this PEP is active, a class may haveannotations defined but hasn’t yet called__annotate__and cached the result, in which case this approachwould lead to mistakenly assuming the class didn’t haveannotations.In any case, the bug was fixed as of Python 3.10, and theidiom should no longer be used. Also as of Python 3.10,there’s anAnnotations HOWTOthat defines best practicesfor working with annotations; code that follows theseguidelines will work correctly even when this PEP isactive, because it suggests using different approachesto get annotations from class objects based on thePython version the code runs under.

Since delaying the evaluation of annotations until they areintrospected changes the semantics of the language, it’s observablefrom within the language. Therefore it’spossible to write codethat behaves differently based on whether annotations areevaluated at binding time or at access time, e.g.

mytype=strdeffoo(a:mytype):passmytype=intprint(foo.__annotations__['a'])

This will print<class'str'> with stock semanticsand<class'int'> when this PEP is active. This istherefore a backwards-incompatible change. However, thisexample is poor programming style, so this change seemsacceptable.

There are two uncommon interactions possible with classand module annotations that work with stock semanticsthat would no longer work when this PEP was active.These two interactions would have to be prohibited. Thegood news is, neither is common, and neither is consideredgood practice. In fact, they’re rarely seen outside ofPython’s own regression test suite. They are:

  • Code that sets annotations on module or class attributesfrom inside any kind of flow control statement. It’scurrently possible to set module and class attributes withannotations inside anif ortry statement, and it worksas one would expect. It’s untenable to support this behaviorwhen this PEP is active.
  • Code in module or class scope that references or modifies thelocal__annotations__dict directly. Currently, whensetting annotations on module or class attributes, the generatedcode simply creates a local__annotations__ dict, then addsmappings to it as needed. It’s possible for user codeto directly modify this dict, though this doesn’t seem to bean intentional feature. Although it would be possible to supportthis after a fashion once this PEP was active, the semanticswould likely be surprising and wouldn’t make anyone happy.

Note that these are both also pain points for static type checkers,and are unsupported by those tools. It seems reasonable todeclare that both are at the very least unsupported, and theiruse results in undefined behavior. It might be worth making asmall effort to explicitly prohibit them with compile-time checks.

Finally, if this PEP is active, annotation values shouldn’t usetheif/else ternary operator. Although this will workcorrectly when accessingo.__annotations__ or requestinginspect.VALUE from a helper function, the boolean expressionmay not compute correctly withinspect.FORWARDREF whensome names are defined, and would be far less correct withinspect.SOURCE.

Backwards Compatibility With PEP 563 Semantics

PEP 563 changed the semantics of annotations. When its semanticsare active, annotations must assume they will be evaluated inmodule-level orclass-level scope. They may no longer refer directlyto local variables in the current function or an enclosing function.This PEP removes that restriction, and annotations may refer anylocal variable.

PEP 563 requires usingeval (or a helper function liketyping.get_type_hints orinspect.get_annotations thatuseseval for you) to convert stringized annotations intotheir “real” values. Existing code that activates stringizedannotations, and callseval() directly to convert the stringsback into real values, can simply remove theeval() call.Existing code using a helper function would continue to workunchanged, though use of those functions may become optional.

Static typing users often have modules that only containinert type hint definitions–but no live code. These modulesare only needed when running static type checking; they aren’tused at runtime. But under stock semantics, these moduleshave to be imported in order for the runtime to evaluate andcompute the annotations. Meanwhile, these modules oftencaused circular import problems that could be difficult oreven impossible to solve.PEP 563 allowed users to solvethese circular import problems by doing two things. First,they activatedPEP 563 in their modules, which meant annotationswere constant strings, and didn’t require the real symbols tobe defined in order for the annotations to be computable.Second, this permitted users to only import the problematicmodules in aniftyping.TYPE_CHECKING block. This allowedthe static type checkers to import the modules and the typedefinitions inside, but they wouldn’t be imported at runtime.So far, this approach will work unchanged when this PEP isactive;iftyping.TYPE_CHECKING is supported behavior.

However, some codebases actuallydid examine theirannotations at runtime, even when using theiftyping.TYPE_CHECKINGtechnique and not importing definitions used in their annotations.These codebases examined the annotation stringswithoutevaluating them, instead relying on identity checks orsimple lexical analysis on the strings.

This PEP supports these techniques too. But users will needto port their code to it. First, user code will need to useinspect.get_annotations ortyping.get_type_hints toaccess the annotations; they won’t be able to simply get the__annotations__ attribute from their object. Second,they will need to specify eitherinspect.FORWARDREForinspect.SOURCE for theformat when calling thatfunction. This means the helper function can succeed inproducing the annotations dict, even when not all the symbolsare defined. Code expecting stringized annotations shouldwork unmodified withinspect.SOURCE formatted annotationsdicts; however, users should consider switching toinspect.FORWARDREF, as it may make their analysis easier.

Similarly,PEP 563 permitted use of class decorators onannotated classes in a way that hadn’t previously been possible.Some class decorators (e.g.dataclasses) examine the annotationson the class. Because class decorators using the@ decoratorsyntax are run before the class name is bound, they can causeunsolvable circular-definition problems. If you annotate attributesof a class with references to the class itself, or annotate attributesin multiple classes with circular references to each other, youcan’t decorate those classes with the@ decorator syntaxusing decorators that examine the annotations.PEP 563 allowedthis to work, as long as the decorators examined the strings lexicallyand didn’t useeval to evaluate them (or handled theNameErrorwith further workarounds). When this PEP is active, decorators willbe able to compute the annotations dict ininspect.SOURCE orinspect.FORWARDREF format using the helper functions. Thiswill permit them to analyze annotations containing undefinedsymbols, in the format they prefer.

Early adopters ofPEP 563 discovered that “stringized”annotations were useful for automatically-generated documentation.Users experimented with this use case, and Python’spydochas expressed some interest in this technique. This PEP supportsthis use case; the code generating the documentation will have to beupdated to use a helper function to access the annotations ininspect.SOURCE format.

Finally, the warnings about using theif/else ternaryoperator in annotations apply equally to users ofPEP 563.It currently works for them, but could produce incorrectresults when requesting some formats from the helper functions.

If this PEP is accepted,PEP 563 will be deprecated andeventually removed. To facilitate this transition for earlyadopters ofPEP 563, who now depend on its semantics,inspect.get_annotations andtyping.get_type_hints willimplement a special affordance.

The Python compiler won’t generate annotation code objectsfor objects defined in a module wherePEP 563 semantics areactive, even if this PEP is accepted. So, under normalcircumstances, requestinginspect.SOURCE format from ahelper function would return an empty dict. As an affordance,to facilitate the transition, if the helper functions detectthat an object was defined in a module withPEP 563 active,and the user requestsinspect.SOURCE format, they’ll returnthe current value of the__annotations__ dict, which inthis case will be the stringized annotations. This will allowPEP 563 users who lexically analyze stringized annotationsto immediately change over to requestinginspect.SOURCE formatfrom the helper functions, which will hopefully smooth theirtransition away fromPEP 563.

Rejected Ideas

“Just store the strings”

One proposed idea for supportingSOURCE format was forthe Python compiler to emit the actual source code for theannotation values somewhere, and to furnish that whenthe user requestedSOURCE format.

This idea wasn’t rejected so much as categorized as“not yet”. We already know we need to supportFORWARDREFformat, and that technique can be adapted to supportSOURCE format in just a few lines. There are manyunanswered questions about this approach:

  • Where would we store the strings? Would they alwaysbe loaded when the annotated object was created, orwould they be lazy-loaded on demand? If so, howwould the lazy-loading work?
  • Would the “source code” include the newlines andcomments of the original? Would it preserve allwhitespace, including indents and extra spaces usedpurely for formatting?

It’s possible we’ll revisit this topic in the future,if improving the fidelity ofSOURCE values to theoriginal source code is judged sufficiently important.

Acknowledgements

Thanks to Carl Meyer, Barry Warsaw, Eric V. Smith,Mark Shannon, Jelle Zijlstra, and Guido van Rossum for ongoingfeedback and encouragement.

Particular thanks to several individuals who contributed key ideasthat became some of the best aspects of this proposal:

  • Carl Meyer suggested the “stringizer” technique that madeFORWARDREF andSOURCE formats possible, whichallowed making forward progress on this PEP possible aftera year of languishing due to seemingly-unfixable problems.He also suggested the affordance forPEP 563 users whereinspect.SOURCE will return the stringized annotations,and many more suggestions besides. Carl was also the primarycorrespondent in private email threads discussing this PEP,and was a tireless resource and voice of sanity. This PEPwould almost certainly not have been accepted it were it notfor Carl’s contributions.
  • Mark Shannon suggested building the entire annotations dictinside a single code object, and only binding it to a functionon demand.
  • Guido van Rossum suggested that__annotate__functions should duplicate the name visibility rules ofannotations under “stock” semantics.
  • Jelle Zijlstra contributed not only feedback–but code!

References

Copyright

This document is placed in the public domain or under theCC0-1.0-Universal license, whichever is more permissive.


Source:https://github.com/python/peps/blob/main/peps/pep-0649.rst

Last modified:2025-10-06 14:23:25 GMT


[8]ページ先頭

©2009-2025 Movatter.jp