Annotations Best Practices¶
- author:
Larry Hastings
Abstract
This document is designed to encapsulate the best practicesfor working with annotations dicts. If you write Python codethat examines__annotations__
on Python objects, weencourage you to follow the guidelines described below.
The document is organized into four sections:best practices for accessing the annotations of an objectin Python versions 3.10 and newer,best practices for accessing the annotations of an objectin Python versions 3.9 and older,other best practicesfor__annotations__
that apply to any Python version,andquirks of__annotations__
.
Note that this document is specifically about working with__annotations__
, not usesfor annotations.If you’re looking for information on how to use “type hints”in your code, please see thetyping
module.
Accessing The Annotations Dict Of An Object In Python 3.10 And Newer¶
Python 3.10 adds a new function to the standard library:inspect.get_annotations()
. In Python versions 3.10and newer, calling this function is the best practice foraccessing the annotations dict of any object that supportsannotations. This function can also “un-stringize”stringized annotations for you.
If for some reasoninspect.get_annotations()
isn’tviable for your use case, you may access the__annotations__
data member manually. Best practicefor this changed in Python 3.10 as well: as of Python 3.10,o.__annotations__
is guaranteed toalways workon Python functions, classes, and modules. If you’recertain the object you’re examining is one of these threespecific objects, you may simply useo.__annotations__
to get at the object’s annotations dict.
However, other types of callables–for example,callables created byfunctools.partial()
–maynot have an__annotations__
attribute defined. Whenaccessing the__annotations__
of a possibly unknownobject, best practice in Python versions 3.10 andnewer is to callgetattr()
with three arguments,for examplegetattr(o,'__annotations__',None)
.
Before Python 3.10, accessing__annotations__
on a class thatdefines no annotations but that has a parent class withannotations would return the parent’s__annotations__
.In Python 3.10 and newer, the child class’s annotationswill be an empty dict instead.
Accessing The Annotations Dict Of An Object In Python 3.9 And Older¶
In Python 3.9 and older, accessing the annotations dictof an object is much more complicated than in newer versions.The problem is a design flaw in these older versions of Python,specifically to do with class annotations.
Best practice for accessing the annotations dict of otherobjects–functions, other callables, and modules–is the sameas best practice for 3.10, assuming you aren’t callinginspect.get_annotations()
: you should use three-argumentgetattr()
to access the object’s__annotations__
attribute.
Unfortunately, this isn’t best practice for classes. The problemis that, since__annotations__
is optional on classes, andbecause classes can inherit attributes from their base classes,accessing the__annotations__
attribute of a class mayinadvertently return the annotations dict of abase class.As an example:
classBase:a:int=3b:str='abc'classDerived(Base):passprint(Derived.__annotations__)
This will print the annotations dict fromBase
, notDerived
.
Your code will have to have a separate code path if the objectyou’re examining is a class (isinstance(o,type)
).In that case, best practice relies on an implementation detailof Python 3.9 and before: if a class has annotations defined,they are stored in the class’s__dict__
dictionary. Sincethe class may or may not have annotations defined, best practiceis to call theget()
method on the class dict.
To put it all together, here is some sample code that safelyaccesses the__annotations__
attribute on an arbitraryobject in Python 3.9 and before:
ifisinstance(o,type):ann=o.__dict__.get('__annotations__',None)else:ann=getattr(o,'__annotations__',None)
After running this code,ann
should be either adictionary orNone
. You’re encouraged to double-checkthe type ofann
usingisinstance()
before furtherexamination.
Note that some exotic or malformed type objects may not havea__dict__
attribute, so for extra safety you may also wishto usegetattr()
to access__dict__
.
Manually Un-Stringizing Stringized Annotations¶
In situations where some annotations may be “stringized”,and you wish to evaluate those strings to produce thePython values they represent, it really is best tocallinspect.get_annotations()
to do this workfor you.
If you’re using Python 3.9 or older, or if for some reasonyou can’t useinspect.get_annotations()
, you’ll needto duplicate its logic. You’re encouraged to examine theimplementation ofinspect.get_annotations()
in thecurrent Python version and follow a similar approach.
In a nutshell, if you wish to evaluate a stringized annotationon an arbitrary objecto
:
If
o
is a module, useo.__dict__
as theglobals
when callingeval()
.If
o
is a class, usesys.modules[o.__module__].__dict__
as theglobals
, anddict(vars(o))
as thelocals
,when callingeval()
.If
o
is a wrapped callable usingfunctools.update_wrapper()
,functools.wraps()
, orfunctools.partial()
, iterativelyunwrap it by accessing eithero.__wrapped__
oro.func
asappropriate, until you have found the root unwrapped function.If
o
is a callable (but not a class), useo.__globals__
as the globals when callingeval()
.
However, not all string values used as annotations canbe successfully turned into Python values byeval()
.String values could theoretically contain any valid string,and in practice there are valid use cases for type hints thatrequire annotating with string values that specificallycan’t be evaluated. For example:
PEP 604 union types using
|
, before support for thiswas added to Python 3.10.Definitions that aren’t needed at runtime, only importedwhen
typing.TYPE_CHECKING
is true.
Ifeval()
attempts to evaluate such values, it willfail and raise an exception. So, when designing a libraryAPI that works with annotations, it’s recommended to onlyattempt to evaluate string values when explicitly requestedto by the caller.
Best Practices For__annotations__
In Any Python Version¶
You should avoid assigning to the
__annotations__
memberof objects directly. Let Python manage setting__annotations__
.If you do assign directly to the
__annotations__
memberof an object, you should always set it to adict
object.If you directly access the
__annotations__
memberof an object, you should ensure that it’s adictionary before attempting to examine its contents.You should avoid modifying
__annotations__
dicts.You should avoid deleting the
__annotations__
attributeof an object.
__annotations__
Quirks¶
In all versions of Python 3, functionobjects lazy-create an annotations dict if no annotationsare defined on that object. You can delete the__annotations__
attribute usingdelfn.__annotations__
, but if you thenaccessfn.__annotations__
the object will create a new empty dictthat it will store and return as its annotations. Deleting theannotations on a function before it has lazily created its annotationsdict will throw anAttributeError
; usingdelfn.__annotations__
twice in a row is guaranteed to always throw anAttributeError
.
Everything in the above paragraph also applies to class and moduleobjects in Python 3.10 and newer.
In all versions of Python 3, you can set__annotations__
on a function object toNone
. However, subsequentlyaccessing the annotations on that object usingfn.__annotations__
will lazy-create an empty dictionary as per the first paragraph ofthis section. This isnot true of modules and classes, in any Pythonversion; those objects permit setting__annotations__
to anyPython value, and will retain whatever value is set.
If Python stringizes your annotations for you(usingfrom__future__importannotations
), and youspecify a string as an annotation, the string willitself be quoted. In effect the annotation is quotedtwice. For example:
from__future__importannotationsdeffoo(a:"str"):passprint(foo.__annotations__)
This prints{'a':"'str'"}
. This shouldn’t really be considereda “quirk”; it’s mentioned here simply because it might be surprising.