Best Practices für Annotationen
*******************************


Zusammenfassung
^^^^^^^^^^^^^^^

Dieses Dokument soll die Best Practices für die Arbeit mit
Annotations-Dictionarys bündeln. Wenn du Python-Code schreibst, der
"__annotations__" an Python-Objekten untersucht, empfehlen wir dir,
den unten beschriebenen Richtlinien zu folgen.

Das Dokument ist in vier Abschnitte gegliedert: Best Practices für den
Zugriff auf die Annotationen eines Objekts in Python 3.10 und neuer,
Best Practices für den Zugriff auf die Annotationen eines Objekts in
Python 3.9 und älter, weitere Best Practices zu "__annotations__", die
für jede Python-Version gelten, und Eigenheiten von "__annotations__".

Beachte, dass es in diesem Dokument ausdrücklich um die Arbeit *mit*
"__annotations__" geht, nicht um Verwendungszwecke *für* Annotationen.
Wenn du Informationen dazu suchst, wie du „Type Hints“ in deinem Code
verwendest, lies bitte beim Modul "typing" nach.


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 "o" ein Modul, verwende beim Aufruf von "eval()" "o.__dict__"
  als "globals".

* Ist "o" eine Klasse, verwende beim Aufruf von "eval()"
  "sys.modules[o.__module__].__dict__" als "globals" und
  "dict(vars(o))" als "locals".

* Ist "o" ein aufrufbares Objekt, das mit
  "functools.update_wrapper()", "@functools.wraps" oder
  "functools.partial()" umhüllt wurde, wickle es Schritt für Schritt
  aus, indem du je nach Fall auf "o.__wrapped__" oder "o.func"
  zugreifst, bis du die zugrunde liegende, nicht umhüllte Funktion
  gefunden hast.

* Ist "o" ein aufrufbares Objekt (aber keine Klasse), verwende beim
  Aufruf von "eval()" "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_CHECKING" wahr 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 ein "dict"-Objekt setzen.

* Du solltest es vermeiden, bei einem Objekt direkt auf
  "__annotations__" zuzugreifen. Verwende stattdessen
  "annotationlib.get_annotations()" (Python 3.14+) oder
  "inspect.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+).
