Best Practices für Annotationen¶
Zugriff auf das Annotations-Dictionary eines Objekts in Python 3.10 und neuer¶
Python 3.10 ergänzt die Standardbibliothek um eine neue Funktion: inspect.get_annotations(). In den Python-Versionen 3.10 bis 3.13 ist der Aufruf dieser Funktion die beste Vorgehensweise, um auf das Annotations-Dictionary eines beliebigen Objekts zuzugreifen, das Annotationen unterstützt. Diese Funktion kann Annotationen, die als Zeichenketten vorliegen, für dich auch wieder zurückwandeln.
In Python 3.14 gibt es ein neues Modul annotationlib mit Funktionalität für die Arbeit mit Annotationen. Dazu gehört eine Funktion annotationlib.get_annotations(), die inspect.get_annotations() ersetzt.
Wenn inspect.get_annotations() für deinen Anwendungsfall aus irgendeinem Grund nicht in Frage kommt, kannst du auf das Datenelement __annotations__ auch von Hand zugreifen. Die beste Vorgehensweise dafür hat sich mit Python 3.10 ebenfalls geändert: Seit Python 3.10 ist garantiert, dass o.__annotations__ bei Python-Funktionen, -Klassen und -Modulen immer funktioniert. Wenn du sicher bist, dass das untersuchte Objekt eines dieser drei bestimmten Objekte ist, kannst du einfach o.__annotations__ verwenden, um an das Annotations-Dictionary des Objekts zu kommen.
Andere Arten von aufrufbaren Objekten – zum Beispiel solche, die von functools.partial() erzeugt werden – haben möglicherweise kein Attribut __annotations__ definiert. Beim Zugriff auf __annotations__ eines möglicherweise unbekannten Objekts ist es in Python 3.10 und neuer die beste Vorgehensweise, getattr() mit drei Argumenten aufzurufen, zum Beispiel getattr(o, '__annotations__', None).
Vor Python 3.10 gab der Zugriff auf __annotations__ bei einer Klasse, die selbst keine Annotationen definiert, aber eine Elternklasse mit Annotationen hat, die __annotations__ der Elternklasse zurück. In Python 3.10 und neuer sind die Annotationen der Kindklasse stattdessen ein leeres Dictionary.
Zugriff auf das Annotations-Dictionary eines Objekts in Python 3.9 und älter¶
In Python 3.9 und älter ist der Zugriff auf das Annotations-Dictionary eines Objekts deutlich umständlicher als in neueren Versionen. Die Ursache ist ein Entwurfsfehler in diesen älteren Python-Versionen, der speziell die Klassenannotationen betrifft.
Die beste Vorgehensweise für den Zugriff auf das Annotations-Dictionary anderer Objekte – Funktionen, andere aufrufbare Objekte und Module – ist dieselbe wie bei 3.10, sofern du inspect.get_annotations() nicht aufrufst: Du solltest getattr() mit drei Argumenten verwenden, um auf das Attribut __annotations__ des Objekts zuzugreifen.
Für Klassen ist das leider nicht die beste Vorgehensweise. Das Problem ist: Da __annotations__ bei Klassen optional ist und Klassen Attribute von ihren Basisklassen erben können, kann der Zugriff auf das Attribut __annotations__ einer Klasse unbeabsichtigt das Annotations-Dictionary einer Basisklasse zurückgeben. Ein Beispiel:
class Base:
a: int = 3
b: str = 'abc'
class Derived(Base):
pass
print(Derived.__annotations__)
Das gibt das Annotations-Dictionary von Base aus, nicht von Derived.
Dein Code braucht einen eigenen Zweig für den Fall, dass das untersuchte Objekt eine Klasse ist (isinstance(o, type)). In diesem Fall stützt sich die beste Vorgehensweise auf ein Implementierungsdetail von Python 3.9 und davor: Wenn eine Klasse Annotationen definiert hat, sind sie im Dictionary __dict__ der Klasse gespeichert. Da die Klasse Annotationen definiert haben kann oder nicht, ist es die beste Vorgehensweise, die Methode get() auf dem Klassen-Dictionary aufzurufen.
Alles zusammengenommen folgt hier Beispielcode, der in Python 3.9 und davor sicher auf das Attribut __annotations__ eines beliebigen Objekts zugreift:
if isinstance(o, type):
ann = o.__dict__.get('__annotations__', None)
else:
ann = getattr(o, '__annotations__', None)
Nach dem Ausführen dieses Codes sollte ann entweder ein Dictionary oder None sein. Es ist ratsam, den Typ von ann mit isinstance() zu prüfen, bevor du weiter damit arbeitest.
Beachte, dass manche exotischen oder fehlerhaften Typobjekte kein Attribut __dict__ haben. Zur zusätzlichen Sicherheit kannst du also auch für den Zugriff auf __dict__ getattr() verwenden.
Annotationen von Hand aus Zeichenketten zurückwandeln¶
In Situationen, in denen manche Annotationen als Zeichenketten vorliegen und du diese Zeichenketten auswerten möchtest, um die Python-Werte zu erhalten, die sie darstellen, ist es wirklich am besten, inspect.get_annotations() aufzurufen und diese Arbeit dort machen zu lassen.
Wenn du Python 3.9 oder älter verwendest oder inspect.get_annotations() aus irgendeinem Grund nicht nutzen kannst, musst du dessen Logik nachbauen. Es ist ratsam, sich die Implementierung von inspect.get_annotations() in der aktuellen Python-Version anzuschauen und ähnlich vorzugehen.
Kurz gefasst: Wenn du eine als Zeichenkette vorliegende Annotation an einem beliebigen Objekt o auswerten möchtest:
Ist
oein Modul, verwende beim Aufruf voneval()o.__dict__alsglobals.Ist
oeine Klasse, verwende beim Aufruf voneval()sys.modules[o.__module__].__dict__alsglobalsunddict(vars(o))alslocals.Ist
oein aufrufbares Objekt, das mitfunctools.update_wrapper(),@functools.wrapsoderfunctools.partial()umhüllt wurde, wickle es Schritt für Schritt aus, indem du je nach Fall aufo.__wrapped__odero.funczugreifst, bis du die zugrunde liegende, nicht umhüllte Funktion gefunden hast.Ist
oein aufrufbares Objekt (aber keine Klasse), verwende beim Aufruf voneval()o.__globals__als globals.
Allerdings lassen sich nicht alle Zeichenkettenwerte, die als Annotationen verwendet werden, von eval() erfolgreich in Python-Werte überführen. Zeichenkettenwerte könnten theoretisch jede gültige Zeichenkette enthalten, und in der Praxis gibt es gültige Anwendungsfälle für Type Hints, die Annotationen mit Zeichenkettenwerten erfordern, die ausdrücklich nicht auswertbar sind. Zum Beispiel:
Union-Typen nach PEP 604 mit
|, bevor die Unterstützung dafür in Python 3.10 ergänzt wurde.Definitionen, die zur Laufzeit nicht gebraucht werden und nur importiert werden, wenn
typing.TYPE_CHECKINGwahr ist.
Versucht eval(), solche Werte auszuwerten, scheitert es und löst eine Ausnahme aus. Wenn du also eine Bibliotheks-API entwirfst, die mit Annotationen arbeitet, ist es empfehlenswert, Zeichenkettenwerte nur dann auszuwerten, wenn der Aufrufer das ausdrücklich verlangt.
Best Practices für __annotations__ in jeder Python-Version¶
Du solltest es vermeiden, dem Element
__annotations__von Objekten direkt etwas zuzuweisen. Lass Python das Setzen von__annotations__verwalten.Wenn du dem Element
__annotations__eines Objekts doch direkt etwas zuweist, solltest du immer eindict-Objekt setzen.Du solltest es vermeiden, bei einem Objekt direkt auf
__annotations__zuzugreifen. Verwende stattdessenannotationlib.get_annotations()(Python 3.14+) oderinspect.get_annotations()(Python 3.10+).Wenn du doch direkt auf das Element
__annotations__eines Objekts zugreifst, solltest du sicherstellen, dass es ein Dictionary ist, bevor du dessen Inhalt untersuchst.Du solltest es vermeiden,
__annotations__-Dictionarys zu verändern.Du solltest es vermeiden, das Attribut
__annotations__eines Objekts zu löschen.
Eigenheiten von __annotations__¶
In allen Versionen von Python 3 erzeugen Funktionsobjekte bei Bedarf ein Annotations-Dictionary, wenn an diesem Objekt keine Annotationen definiert sind. Du kannst das Attribut __annotations__ mit del fn.__annotations__ löschen. Greifst du danach aber auf fn.__annotations__ zu, erzeugt das Objekt ein neues leeres Dictionary, das es speichert und als seine Annotationen zurückgibt. Löschst du die Annotationen einer Funktion, bevor sie ihr Annotations-Dictionary bei Bedarf erzeugt hat, wird ein AttributeError ausgelöst; del fn.__annotations__ zweimal hintereinander löst garantiert immer einen AttributeError aus.
Alles im obigen Absatz gilt in Python 3.10 und neuer auch für Klassen- und Modulobjekte.
In allen Versionen von Python 3 kannst du __annotations__ an einem Funktionsobjekt auf None setzen. Greifst du anschließend über fn.__annotations__ auf die Annotationen dieses Objekts zu, wird gemäß dem ersten Absatz dieses Abschnitts allerdings bei Bedarf ein leeres Dictionary erzeugt. Für Module und Klassen gilt das in keiner Python-Version. Diese Objekte erlauben es, __annotations__ auf einen beliebigen Python-Wert zu setzen, und behalten den gesetzten Wert.
Wenn Python deine Annotationen für dich in Zeichenketten umwandelt (über from __future__ import annotations) und du eine Zeichenkette als Annotation angibst, wird die Zeichenkette selbst in Anführungszeichen gesetzt. Die Annotation wird damit zweifach in Anführungszeichen gesetzt. Zum Beispiel:
from __future__ import annotations
def foo(a: "str"): pass
print(foo.__annotations__)
Das gibt {'a': "'str'"} aus. Das sollte man nicht wirklich als „Eigenheit“ betrachten. Es wird hier nur erwähnt, weil es überraschen kann.
Wenn du eine Klasse mit einer eigenen Metaklasse verwendest und bei der Klasse auf __annotations__ zugreifst, kann unerwartetes Verhalten auftreten. Einige Beispiele dazu findest du in 749. Du kannst diese Eigenheiten vermeiden, indem du unter Python 3.14+ annotationlib.get_annotations() oder unter Python 3.10+ inspect.get_annotations() verwendest. In früheren Python-Versionen kannst du diese Fehler vermeiden, indem du die Annotationen über __dict__ der Klasse ausliest (zum Beispiel cls.__dict__.get('__annotations__', None)).
In manchen Python-Versionen können Instanzen von Klassen ein Attribut __annotations__ haben. Das ist allerdings keine unterstützte Funktionalität. Wenn du die Annotationen einer Instanz brauchst, kannst du mit type() auf deren Klasse zugreifen (zum Beispiel annotationlib.get_annotations(type(myinstance)) unter Python 3.14+).