Python-Typannotationen (Type Hints) ändern die Laufzeit nicht – sie sind eine Beschreibung für Menschen und Werkzeuge. Der Nutzen entsteht durch die Prüfung im Editor und in der Pipeline: Fehler werden gefunden, bevor das Programm läuft.

\n

Grundlagen

\n
# Variablen und Funktionen\nname: str = "web01"\nzaehler: int = 0\naktiv: bool = False\nwerte: list[int] = []\nzuordnung: dict[str, int] = {}\n\ndef begruesse(name: str, laut: bool = False) -> str:\n    return f"Hallo {name}" if not laut else f"HALLO {name.upper()}!"\n\ndef summiere(zahlen: list[float]) -> float:\n    return sum(zahlen)\n\ndef hole_server(hostname: str) -> dict[str, str] | None:\n    "Gibt None zurueck, wenn der Server nicht existiert."\n    ...\n\n# Typen koennen verschiedene Formen annehmen\nfrom typing import Optional, Union, Any\n\n# Beides gleichbedeutend (ab Python 3.10 ist die Kurzform ueblicher):\ndef a(x: Optional[str]) -> None: ...        # alt\ndef b(x: str | None) -> None: ...           # ab 3.10, klarer\n\ndef c(x: Union[int, str]) -> None: ...      # alt\ndef d(x: int | str) -> None: ...            # ab 3.10\n\n# Ab Python 3.9 sind die eingebauten Typen direkt verwendbar:\n#   list[int]  statt  List[int]      (aus typing)\n#   dict[str, int]  statt  Dict[str, int]\n#   tuple[int, str]  statt  Tuple[int, str]\n#   set[str]  statt  Set[str]\n# from __future__ import annotations   # macht Annotationen ueberall zu Strings\n#   -> noetig, wenn man sehr neue Syntax in aelteren Versionen nutzen will
\n

Werkzeuge: die Prüfung

\n
# 1. Statische Pruefung - ohne Ausfuehrung des Programms\npip install mypy\nmypy app.py\nmypy --strict app.py                    # alle strikten Pruefungen\nmypy app.py --ignore-missing-imports    # ohne Stubs fuer Bibliotheken\n\n# 2. Schnellerer Pruefer, gut fuer die Pipeline\npip install pyright\npyright app.py\n\n# 3. Editiereinbindung - der groesste Nutzen im Alltag\n#   VS Code: Pylance (bringt pyright mit)\n#   Neovim: pyright oder mypy per LSP\n#   -> Fehler erscheinen, WAEHREND man schreibt, nicht erst im Test\n\n# Beispielhafter Fehler, den mypy findet:\ndef verarbeite(zahlen: list[int]) -> int:\n    return sum(zahlen)\n\nverarbeite([1, 2, 3])          # ok\nverarbeite(["a", "b"])         # Fehler: list[str] ist nicht list[int]\nverarbeite("123")              # Fehler: str ist nicht list[int]\n#   Alle drei Varianten laufen in Python OHNE mypy bis zum Absturz\n#   oder - schlimmer - bis zu einem falschen Ergebnis durch.
\n

Fortgeschrittene Typen

\n
from collections.abc import Iterable, Callable, Iterator, Sequence\nfrom typing import Protocol, TypedDict, Literal, Annotated, TypeVar\n\n# Eingabe: "ich akzeptiere alles Aufzaehlbare" - nicht nur eine Liste\ndef mittel(werte: Iterable[float]) -> float:\n    werte = list(werte)                # einmal in eine Liste wandeln\n    return sum(werte) / len(werte)\n\nmittel([1.0, 2.0])\nmittel({1.0, 2.0})                    # Menge - funktioniert auch\nmittel(x for x in range(5))           # Generator - kein Speicherverbrauch\n\n# Ausgabe: "ich liefere einen Iterator" - wichtig fuer grosse Daten\ndef lies_zeilen(pfad: str) -> Iterator[str]:\n    with open(pfad, encoding="utf-8") as f:\n        for zeile in f:\n            yield zeile.rstrip("\\n")\n\n# Funktionen als Parameter\ndef anwenden(werte: list[int], f: Callable[[int], int]) -> list[int]:\n    return [f(w) for w in werte]\n\nanwenden([1, 2, 3], lambda x: x * 2)\n\n# Strukturierte Wörterbücher statt dict[str, object]\nclass Server(TypedDict):\n    name: str\n    ip: str\n    port: int\n\ndef verbinde(server: Server) -> None:\n    print(server["name"], server["ip"], server["port"])\n#   Tippfehler in den Schluesseln "name"/"ip"/"port" werden gemeldet!\n\n# Genau festgelegte Werte\nModus = Literal["lesen", "schreiben", "anhaengen"]\n\ndef oeffne(pfad: str, modus: Modus = "lesen") -> None: ...\noeffne("x.txt", "schreiben")          # ok\noeffne("x.txt", "schreiben!")         # Fehler - nicht im Literal\n\n# Eigenes Verhalten beschreiben (Protocol statt Vererbung!)\nclass Schliessbar(Protocol):\n    def schliessen(self) -> None: ...\n\ndef aufraeumen(objekte: list[Schliessbar]) -> None:\n    for o in objekte:\n        o.schliessen()\n#   Jede Klasse mit passender schliessen()-Methode passt -\n#   ohne von Schliessbar zu erben. Sehr praktisch in der Praxis.\n\n# Typvariablen fuer generische Funktionen\nT = TypeVar("T")\n\ndef erstes(werte: Sequence[T]) -> T | None:\n    return werte[0] if werte else None\n#   mypy leitet ab: erstes([1,2]) -> int | None\n#                   erstes(["a"]) -> str | None\n\n# Zusaetzliche Angaben, die nicht zur Laufzeit geprueft werden\nfrom typing import Annotated\n\ndef hole(pfad: Annotated[str, "absoluter Pfad ohne '..'"],\n         zeitlimit: Annotated[float, "in Sekunden"] = 5.0) -> bytes:\n    ...\n#   Die Zusatzangaben sind dokumentierend (oder fuer Werkzeuge wie\n#   pydantic nutzbar) und veraendern das Verhalten nicht.
\n

Was Typannotationen nicht leisten

\n
    \n
  • Keine Prüfung zur Laufzeit. Python prüft die Annotationen nicht. verarbeite("123") läuft ohne Fehler durch, wenn man nur die Annotation ändert. Die Prüfung muss ein statisches Werkzeug übernehmen.
  • \n
  • Keine Typumwandlung. zahlen: list[int] = input() – die Annotation wandelt nichts. Das ist ein häufiges Missverständnis bei Einsteigern.
  • \n
  • Keine Garantie bei Daten von außen. Eine JSON-Antwort ist zur Laufzeit unbekannt. Dafür braucht es echte Prüfung (pydantic, attrs, oder eigene Prüfungen).
  • \n
  • Keine Leistungsverbesserung. Annotationen machen Programme nicht schneller. (Ausnahme: Werkzeuge wie Numba oder mypyc können sie nutzen.)
  • \n
\n

Prüfung zur Laufzeit, wo sie nötig ist

\n
# Pydantic ist der Standard fuer Daten von aussen (APIs, Dateien, Formulare)\nfrom pydantic import BaseModel, Field, field_validator\n\nclass Server(BaseModel):\n    name: str = Field(min_length=1, max_length=64)\n    ip: str\n    port: int = Field(ge=1, le=65535)\n\n    @field_validator("ip")\n    @classmethod\n    def ip_muss_gueltig_sein(cls, v: str) -> str:\n        import ipaddress\n        try:\n            ipaddress.ip_address(v)      # jetzt wird wirklich geprueft!\n        except ValueError as fehler:\n            raise ValueError(f"ungueltige IP: {v}") from fehler\n        return v\n\n# Aus JSON erzeugen - mit Pruefung und klarer Fehlermeldung\nroh = '{"name": "web01", "ip": "10.0.0.11", "port": 8080}'\nserver = Server.model_validate_json(roh)      # wirft bei Fehlern\nprint(server.name, server.ip, server.port)\nprint(server.model_dump_json())\n\n# Fuer Daten aus dem eigenen Programm genuegt mypy.\n# Fuer Daten von aussen ist eine echte Pruefung Pflicht.
\n

Umgang in bestehenden Projekten

\n
    \n
  1. Nicht alles auf einmal. mypy in einem gewachsenen Projekt erzeugt hunderte Meldungen. Sinnvoll ist ein stufenweises Vorgehen.
  2. \n
  3. Loslassen, was nicht passt: Einzelne Module oder Zeilen übergehen:\n
    # mypy.ini\n[mypy]\npython_version = 3.11\nwarn_unused_ignores = True\ndisallow_untyped_defs = False        # erst spaeter einschalten\n\n[mypy-altes_modul.*]\nignore_errors = True\n#   Neue Module sofort streng pruefen, alte bei Gelegenheit nachziehen.
  4. \n
  5. Fehler einzeln bewerten, nicht pauschal ignorieren. # type: ignore ohne Kommentar ist ein versteckter Fehler. Mit Grund ist es vertretbar:\n
    wert = daten["gemischt"]  # type: ignore[index]  # API liefert leider gemischte Typen\n#   Mit "warn_unused_ignores = True" meldet mypy, wenn eine\n#   Uebergehung nicht mehr noetig ist - dann entfernen.
  6. \n
  7. In die Pipeline aufnehmen. Ohne Durchsetzung veralten Annotationen und werden irreführend. Ein Prüflauf bei jedem Änderungssatz hält den Bestand sauber.
  8. \n
  9. Klare Namen verwenden, keine Any-Flucht. Wer überall Any schreibt, hat den Nutzen aufgegeben. object oder Protocol sind meist die bessere Wahl.
  10. \n
\n

Kurz: was sofort Nutzen bringt

\n
# Selbst ohne meine strenge Pruefung sind diese Annotationen sofort wertvoll:\ndef verarbeite(pfad: str, zeitlimit: float = 5.0) -> list[dict[str, str]]:\n    "Liest Konfigurationseintraege aus einer Datei."\n    ...\n\n# Sie zeigen im Editor:\n#   - welche Parameter eine Funktion erwartet und welche Typen sie hat\n#   - was sie zurueckgibt (auch: dass sie NICHTS zurueckgibt)\n#   - ob ein Attribut existiert, das man gerade schreiben will\n#   - unbekannte oder falsch geschriebene Parameter beim Aufruf\n\n# Der groesste praktische Vorteil ist nicht die Pruefung,\n# sondern die Vervollstaendigung und das Nachschlagen im Editor.\n# Deshalb lohnt es sich auch in kleineren Skripten.
\n

Verwandte Grundlagen: Python: Variablen und Datentypen, pytest, Python-Befehle, Code-Review.