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:

  • Ifo is a module, useo.__dict__ as theglobals when callingeval().

  • Ifo is a class, usesys.modules[o.__module__].__dict__as theglobals, anddict(vars(o)) as thelocals,when callingeval().

  • Ifo 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.

  • Ifo 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 importedwhentyping.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.