cat posts/type-hints-mypy-pyright.md
Type Hints in der Praxis: mypy, Pyright und Co.
Typen annotieren, ohne Pythons Dynamik zu verlieren: moderne Type-Hint-Syntax, typische Stolperfallen und der praktische Einstieg mit mypy, Pyright, ty und Pyrefly.
In Teil 11 der Python-Serie kamen Type Hints als Ausblick vor.
Python bleibt eine dynamische Sprache. Du kannst einer Variablen erst eine Zahl zuweisen und später einen String:
wert = 10
wert = "zehn"
Type Hints ändern daran zunächst nichts. Sie sind optionale Annotationen, die beschreiben, welche Typen erwartet werden.
def erleide_schaden(hp: int, menge: int) -> int:
return max(hp - menge, 0)
Python selbst verhindert dadurch zur Laufzeit nicht automatisch falsche Werte. Aber Werkzeuge können diese Annotationen auswerten und Fehler finden, bevor Du das Programm startest.
Genau darum geht es in diesem Artikel:
- moderne Type-Hint-Syntax,
- typische Typen für Funktionen und Collections,
None, Union Types und Type Aliases,- eigene Klassen als Typen,
- mypy, Pyright, ty und Pyrefly,
- sinnvolle schrittweise Einführung in echten Projekten.
Type Hints sind kein Laufzeitschutz
Diese Funktion erwartet laut Annotation zwei Integer und gibt einen Integer zurück:
def erleide_schaden(hp: int, menge: int) -> int:
return max(hp - menge, 0)
Ein korrekter Aufruf:
neue_hp = erleide_schaden(100, 30)
print(neue_hp)
Ausgabe:
70
Dieser Aufruf ist laut Type Hint falsch:
neue_hp = erleide_schaden("viel", 30)
Python verbietet ihn aber nicht direkt beim Funktionsaufruf.
Der Code läuft erst einmal los und scheitert später an der Operation:
hp - menge
weil ein String nicht sinnvoll mit einem Integer subtrahiert werden kann.
Type Hints sind deshalb keine automatische Validierung. Sie sind Informationen für:
- Leser des Codes,
- Editoren,
- Autocompletion,
- Refactoring-Werkzeuge,
- statische Type Checker,
- Dokumentation.
Wenn Du Laufzeitvalidierung brauchst, musst Du sie selbst schreiben oder ein Werkzeug verwenden, das genau dafür gedacht ist.
Funktionen annotieren
Parameter annotierst Du mit einem Doppelpunkt:
def begruesse(name: str):
print(f"Hallo, {name}!")
Den Rückgabewert annotierst Du mit ->:
def verdopple(zahl: int) -> int:
return zahl * 2
Eine Funktion, die nichts Sinnvolles zurückgibt, hat den Rückgabetyp None:
def zeige_status(text: str) -> None:
print(text)
None als Rückgabewert bedeutet:
Diese Funktion ist für ihren Effekt da, nicht für einen berechneten Wert.
Zum Beispiel:
def zeige_inventar(inventar: list[str]) -> None:
for gegenstand in inventar:
print(gegenstand)
Eine Funktion mit Rückgabewert:
def inventar_text(inventar: list[str]) -> str:
return ", ".join(inventar)
Die erste Funktion gibt direkt aus. Die zweite baut einen String und gibt ihn zurück.
Dieser Unterschied ist für Tests und Wiederverwendbarkeit wichtig.
Variablen annotieren
Auch Variablen können annotiert werden:
name: str = "Karl"
hp: int = 100
lebendig: bool = True
Das ist nicht immer notwendig. Type Checker können viele Typen selbst ableiten.
name = "Karl" # klar: str
hp = 100 # klar: int
Nützlich sind Variablenannotationen vor allem, wenn der Typ nicht offensichtlich ist:
inventar: list[str] = []
Ohne Annotation weiß ein Type Checker bei einer leeren Liste zunächst nicht, was später hineingehören soll.
Auch bei None als Startwert ist eine Annotation hilfreich:
aktueller_raum: Raum | None = None
Damit sagst Du:
Diese Variable enthält später entweder einen
RaumoderNone.
Collections annotieren
Für Listen, Dictionaries, Sets und Tupel verwendest Du eckige Klammern.
namen: list[str] = ["Karl", "Lara"]
leben: dict[str, int] = {"Karl": 100, "Lara": 80}
besuchte_raeume: set[str] = {"halle", "bibliothek"}
position: tuple[int, int] = (3, 7)
Die Schreibweise
list[str]
dict[str, int]
set[str]
tuple[int, int]
ist die moderne Form für aktuelle Python-Versionen.
Ältere Tutorials verwenden oft:
from typing import List, Dict, Set, Tuple
namen: List[str] = ["Karl", "Lara"]
Das war früher notwendig. In modernem Python ist die eingebaute Schreibweise meist besser lesbar.
Listen, Sequences und Iterables
Manchmal ist list[str] zu konkret.
Diese Funktion braucht eigentlich keine Liste. Sie muss nur über Namen iterieren:
def zeige_namen(namen: list[str]) -> None:
for name in namen:
print(name)
Sie funktioniert aber auch mit einem Tupel oder einem Set:
zeige_namen(("Karl", "Lara"))
zeige_namen({"Karl", "Lara"})
Dann ist ein allgemeinerer Typ passender:
from collections.abc import Iterable
def zeige_namen(namen: Iterable[str]) -> None:
for name in namen:
print(name)
Iterable[str] bedeutet:
Etwas, über das man Strings iterieren kann.
Wenn die Funktion eine Reihenfolge und Indexzugriff braucht, passt
Sequence[str]:
from collections.abc import Sequence
def erster_name(namen: Sequence[str]) -> str:
return namen[0]
Eine list[str] ist eine Sequence. Ein tuple[str, ...] ebenfalls. Ein set[str]
nicht, weil Sets keinen stabilen Indexzugriff haben.
Als grobe Orientierung:
- Nimm
list[T], wenn Du wirklich eine Liste brauchst oder sie veränderst. - Nimm
Sequence[T], wenn Du Indexzugriff und Länge brauchst. - Nimm
Iterable[T], wenn Du nur darüber iterierst. - Nimm
Mapping[K, V], wenn Du nur lesend mit einem dict-artigen Objekt arbeitest.
Tupel genauer annotieren
Ein Tupel mit genau zwei Integern:
position: tuple[int, int] = (3, 7)
Ein Tupel mit beliebig vielen Strings:
namen: tuple[str, ...] = ("Karl", "Lara", "Mira")
Die drei Punkte bedeuten:
beliebig viele Elemente dieses Typs.
Ohne die Punkte beschreibt tuple[str] ein Tupel mit genau einem String.
ein_name: tuple[str] = ("Karl",)
Das Komma ist bei einem ein-elementigen Tupel notwendig.
None und Union Types
Wenn ein Wert fehlen kann, verwendest Du | None:
def finde_gegenstand(name: str) -> Gegenstand | None:
...
Das bedeutet:
Die Funktion gibt entweder einen
Gegenstandzurück oderNone.
Verwendung:
gegenstand = finde_gegenstand("Fackel")
if gegenstand is None:
print("Nicht gefunden.")
else:
print(gegenstand.name)
Der if-Block ist nicht nur zur Laufzeit sinnvoll. Type Checker verstehen auch:
- Vor der Prüfung:
Gegenstand | None - Im
else-Block: sicherGegenstand
Das nennt man häufig Narrowing.
Mehrere mögliche Typen
Mit | kannst Du mehrere Typen erlauben:
def beschreibe_wert(wert: int | str) -> str:
return str(wert)
Oder:
def lade_id(wert: int | str) -> int:
return int(wert)
Solche Union Types solltest Du nicht zu großzügig verwenden.
Wenn eine Funktion zu viele mögliche Typen akzeptiert, wird ihr Verhalten oft schwerer verständlich.
Besser ist häufig, Eingaben früh zu normalisieren:
def lade_id(wert: str) -> int:
return int(wert)
Dann arbeiten die inneren Funktionen mit klaren Typen.
Type Aliases
Manche Typen werden lang und wiederholen sich.
dict[str, list[tuple[str, int]]]
Dafür kannst Du einen Type Alias verwenden.
Seit Python 3.12 gibt es dafür das type-Statement:
type InventarDaten = list[str]
type RaumId = str
type Spielstand = dict[str, object]
Ein einfacheres und nützlicheres Beispiel:
type RaumId = str
type GegenstandsId = str
def bewege(ort: RaumId, ziel: RaumId) -> RaumId:
return ziel
Ein Type Alias erzeugt keinen neuen Laufzeittyp. Für den Type Checker ist
RaumId hier weiterhin ein str.
Der Alias macht aber die Bedeutung klarer.
Für ältere Python-Versionen sieht man oft:
RaumId = str
Oder ausdrücklich:
from typing import TypeAlias
RaumId: TypeAlias = str
In modernem Python ist type RaumId = str die klarste Schreibweise.
Literal: konkrete erlaubte Werte
Wenn nur bestimmte Strings erlaubt sind, kann Literal sinnvoll sein:
from typing import Literal
Schwierigkeit = Literal["normal", "schwer", "alptraum"]
def setze_schwierigkeit(modus: Schwierigkeit) -> None:
print(f"Modus: {modus}")
Gültig:
setze_schwierigkeit("schwer")
Ein Type Checker kann diesen Aufruf bemängeln:
setze_schwierigkeit("brutal")
Literal ist nützlich bei kleinen festen Wertemengen:
- Modi,
- Statuswerte,
- Richtungen,
- Konfigurationsoptionen.
Für größere oder fachlich wichtige Wertemengen ist ein Enum oft besser.
Eigene Klassen als Typen
Klassen kannst Du direkt als Typ verwenden.
class Spieler:
def __init__(self, name: str, max_hp: int = 100) -> None:
self.name = name
self.hp = max_hp
self.max_hp = max_hp
def heile_spieler(spieler: Spieler, menge: int) -> None:
spieler.hp = min(spieler.hp + menge, spieler.max_hp)
Bei mehreren Klassen sieht das zum Beispiel so aus:
class Gegenstand:
def __init__(self, name: str) -> None:
self.name = name
class Raum:
def __init__(self, name: str, gegenstaende: list[Gegenstand]) -> None:
self.name = name
self.gegenstaende = gegenstaende
Type Hints dokumentieren hier nicht nur primitive Werte, sondern auch die Beziehungen zwischen Deinen eigenen Objekten.
Attribute annotieren
Attribute entstehen in Python häufig im __init__.
class Spieler:
def __init__(self, name: str, max_hp: int = 100) -> None:
self.name = name
self.hp = max_hp
self.max_hp = max_hp
self.inventar = []
Bei self.inventar = [] ist nicht klar, welche Elemente in die Liste gehören.
Besser:
class Spieler:
def __init__(self, name: str, max_hp: int = 100) -> None:
self.name = name
self.hp = max_hp
self.max_hp = max_hp
self.inventar: list[Gegenstand] = []
Oder auf Klassenebene:
class Spieler:
name: str
hp: int
max_hp: int
inventar: list[Gegenstand]
def __init__(self, name: str, max_hp: int = 100) -> None:
self.name = name
self.hp = max_hp
self.max_hp = max_hp
self.inventar = []
Bei Dataclasses sind Annotationen Teil der Klassendefinition:
from dataclasses import dataclass, field
@dataclass
class Spieler:
name: str
max_hp: int = 100
hp: int = field(init=False)
inventar: list[Gegenstand] = field(default_factory=list)
def __post_init__(self) -> None:
self.hp = self.max_hp
Hier sind Type Hints besonders natürlich, weil Dataclasses ihre Fields aus den Annotationen ableiten.
Methoden und Self
Manche Methoden geben das eigene Objekt zurück.
class Spieler:
def setze_gold(self, gold: int):
self.gold = gold
return self
Für solche Methoden gibt es Self:
from typing import Self
class Spieler:
def __init__(self, name: str) -> None:
self.name = name
self.gold = 0
def setze_gold(self, gold: int) -> Self:
self.gold = gold
return self
Das ist besonders nützlich bei verkettbaren Methoden:
spieler.setze_gold(50).setze_gold(100)
Self bedeutet:
Rückgabewert ist dieselbe konkrete Klasse wie
self.
Das ist präziser als einfach -> Spieler.
Callables: Funktionen als Werte
Funktionen können in Python als Argumente übergeben werden.
def sortiere_namen(namen: list[str], key):
return sorted(namen, key=key)
Mit Type Hint:
from collections.abc import Callable
def sortiere_namen(
namen: list[str],
key: Callable[[str], int],
) -> list[str]:
return sorted(namen, key=key)
Callable[[str], int] bedeutet:
Eine Funktion, die einen String nimmt und einen Integer zurückgibt.
Beispiel:
namen = ["Karl", "Alexandra", "Mia"]
sortiert = sortiere_namen(namen, key=len)
len passt, weil len(str) einen Integer zurückgibt.
Für komplexere Callables können Type Hints schnell unübersichtlich werden. Dann
ist ein Protocol oft besser.
Protocol: Verhalten statt konkrete Klasse
Manchmal ist nicht wichtig, welche Klasse ein Objekt hat. Wichtig ist nur, dass es bestimmte Methoden besitzt.
Beispiel:
def zeige(objekt):
print(str(objekt))
Alles, was sinnvoll in einen String umgewandelt werden kann, passt.
Für eigene Protokolle gibt es Protocol:
from typing import Protocol
class Beschreibbar(Protocol):
def beschreibe(self) -> str:
...
def zeige_beschreibung(objekt: Beschreibbar) -> None:
print(objekt.beschreibe())
Jede Klasse mit passender Methode erfüllt dieses Protokoll:
class Raum:
def beschreibe(self) -> str:
return "Eine dunkle Halle."
class Gegenstand:
def beschreibe(self) -> str:
return "Eine rußige Fackel."
Beide können an zeige_beschreibung(...) übergeben werden.
Das nennt man strukturelles Typing oder Duck Typing mit statischer Prüfung.
Any: der Ausweg mit Nebenwirkungen
Any bedeutet:
Der Type Checker soll hier praktisch alles erlauben.
from typing import Any
def lade_rohdaten() -> Any:
...
Mit Any kannst Du jeden Typfehler durchlassen.
wert: Any = "viel"
wert - 3
wert.unbekannte_methode()
Ein Type Checker wird hier deutlich weniger warnen, weil Any die Prüfung
lockert.
Any ist manchmal notwendig:
- bei untypisierten Drittbibliotheken,
- bei dynamischen Daten,
- bei schrittweiser Migration,
- an klar abgegrenzten Rändern des Programms.
Aber zu viel Any nimmt Type Hints ihren Nutzen.
Eine gute Strategie:
Anymöglichst am Rand halten und Daten früh in klare Typen umwandeln.
cast: dem Checker etwas versprechen
Manchmal weißt Du mehr als der Type Checker.
from typing import cast
wert: object = "Karl"
name = cast(str, wert)
cast(str, wert) sagt dem Type Checker:
Behandle
wertab hier alsstr.
Zur Laufzeit prüft cast den Wert nicht. Es ist nur eine Information für den
Checker.
Das hier ist gefährlich:
wert: object = 123
name = cast(str, wert)
print(name.upper())
Der Type Checker ist vielleicht zufrieden, aber zur Laufzeit scheitert der Code,
weil ein Integer keine Methode .upper() besitzt.
Verwende cast sparsam. Oft ist eine echte Prüfung besser:
if isinstance(wert, str):
print(wert.upper())
Type Narrowing mit isinstance
Type Checker verstehen viele normale Python-Prüfungen.
def normalisiere(wert: int | str) -> int:
if isinstance(wert, int):
return wert
return int(wert)
Vor dem if ist wert:
int | str
Im if-Block ist wert sicher:
int
Im Rest der Funktion bleibt dann:
str
Solche Prüfungen sind nützlich, wenn Du an den Rändern des Programms flexible Eingaben akzeptierst und danach mit klaren Typen weiterarbeiten möchtest.
Type Hints im Dungeon
Einige Funktionen aus dem Dungeon könnten so aussehen:
from pathlib import Path
def frage_ganzzahl(text: str, minimum: int | None = None) -> int:
...
def teile_befehl(eingabe: str) -> tuple[str, str]:
...
def finde_raeume_mit_loot(raeume: dict[str, Raum]) -> list[str]:
...
def speichere_spielstand(
spieler: Spieler,
ort: str,
raeume: dict[str, Raum],
besuchte_raeume: set[str],
speicherdatei: Path,
) -> None:
...
def lade_spielstand(
spieler: Spieler,
raeume: dict[str, Raum],
speicherdatei: Path,
) -> tuple[str, set[str]]:
...
Dadurch wird die API der Funktionen klarer.
Du siehst direkt:
- welche Werte hineingehen,
- was zurückkommt,
- welche Datenstrukturen erwartet werden.
Ein typisierter Ausschnitt
Nehmen wir eine kleine Version der Befehlszerlegung:
from dataclasses import dataclass
@dataclass(frozen=True)
class Befehl:
verb: str
argument: str = ""
def teile_befehl(eingabe: str) -> Befehl:
verb, _, argument = eingabe.strip().lower().partition(" ")
if not verb:
raise ValueError("Bitte gib einen Befehl ein.")
return Befehl(
verb=verb,
argument=argument.strip(),
)
Verwendung:
befehl = teile_befehl("nimm fackel")
print(befehl.verb)
print(befehl.argument)
Ein Type Checker weiß nun:
teile_befehl(...)erwartet einen String.- Die Funktion gibt einen
Befehlzurück. befehl.verbist ein String.befehl.argumentist ein String.
Würdest Du schreiben:
befehl = teile_befehl(123)
kann ein Type Checker warnen, bevor das Programm läuft.
mypy installieren und ausführen
mypy ist einer der etabliertesten statischen Type Checker für Python.
Mit uv:
uv add --dev mypy
Ausführen:
uv run mypy .
Klassisch in einer aktivierten venv:
python -m pip install mypy
python -m mypy .
Du kannst auch eine einzelne Datei prüfen:
python -m mypy modelle.py
Ein typischer Fehler könnte so aussehen:
def erleide_schaden(hp: int, menge: int) -> int:
return max(hp - menge, 0)
erleide_schaden("viel", 3)
mypy meldet sinngemäß:
Argument 1 to "erleide_schaden" has incompatible type "str"; expected "int"
Der genaue Text hängt von Version und Konfiguration ab.
mypy konfigurieren
Für kleine Tests reicht der direkte Aufruf.
In einem Projekt gehört die Konfiguration besser in die pyproject.toml:
[tool.mypy]
python_version = "3.13"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_untyped_defs = true bedeutet:
Funktionen ohne Typannotationen werden gemeldet.
Das kann am Anfang zu streng sein.
Für einen sanfteren Einstieg:
[tool.mypy]
python_version = "3.13"
warn_unused_configs = true
check_untyped_defs = true
check_untyped_defs = true prüft die Körper unannotierter Funktionen überhaupt
erst – standardmäßig lässt mypy sie ungeprüft –, verlangt aber nicht sofort
überall vollständige Signaturen.
mypy strict
mypy besitzt einen strengen Modus:
uv run mypy --strict .
Oder in pyproject.toml:
[tool.mypy]
strict = true
strict aktiviert viele einzelne Prüfregeln.
Das ist für neue, bewusst typisierte Projekte sinnvoll. Für ältere oder große untypisierte Projekte kann es frustrierend sein.
Besser ist oft:
- mypy zunächst ohne
stricteinführen. - Wichtige Module annotieren.
- Einzelne strengere Optionen aktivieren.
- Fehler Schritt für Schritt abbauen.
strictnur dort aktivieren, wo es realistisch ist.
Type Checking ist ein Werkzeug zur Verbesserung, kein Selbstzweck.
Pyright und Pylance
Pyright ist ein statischer Type Checker von Microsoft.
In VS Code begegnet er vielen Entwicklern indirekt über Pylance. Pylance nutzt Pyright für Type Checking und ergänzt weitere Editorfunktionen.
Das bedeutet:
Wenn Du in VS Code mit Pylance arbeitest, bekommst Du viele Pyright-Hinweise direkt im Editor.
Pyright kann auch auf der Kommandozeile laufen.
Je nach Setup:
pyright
Oder als Projektabhängigkeit beziehungsweise Tool über Dein gewähltes Werkzeug.
Eine einfache pyrightconfig.json könnte so aussehen:
{
"typeCheckingMode": "standard",
"include": ["."]
}
standard ist seit 2024 die Voreinstellung. basic ist die bewusst mildere
Stufe, daneben gibt es noch off.
Strenger:
{
"typeCheckingMode": "strict",
"include": ["."]
}
In VS Code lässt sich der Type-Checking-Modus auch über Einstellungen steuern.
mypy oder Pyright?
Beide Werkzeuge sind verbreitet und sinnvoll.
mypy passt gut, wenn …
- Du möglichst nahe an vielen bestehenden Python-Projekten bleiben willst.
- Tutorials, Libraries oder Teams bereits mypy verwenden.
- Du granular über Konfigurationsoptionen arbeiten möchtest.
- Du ein etabliertes CLI-Tool für CI suchst.
Pyright passt gut, wenn …
- Du VS Code mit Pylance nutzt.
- Du schnelle Rückmeldungen direkt im Editor möchtest.
- Du moderne Typing-Features früh verwenden willst.
- Dein Team bereits auf Pyright oder Pylance standardisiert ist.
In vielen Projekten reicht ein Checker.
Zwei Checker parallel können mehr Fehler finden, aber auch mehr Konfigurationsaufwand erzeugen. Für den Einstieg ist es besser, einen Checker konsequent einzurichten.
Die neue Generation: ty und Pyrefly
Neben mypy und Pyright gibt es neuere Type Checker, die auf Geschwindigkeit und moderne Tooling-Integration setzen.
ty
ty kommt von Astral, den Entwicklern hinter Ruff und uv.
Es ist ein schneller Type Checker und Language Server in Rust.
Typischer Aufruf:
ty check
Mit uv:
uvx ty check
Oder als Development Dependency:
uv add --dev ty
uv run ty check
ty ist besonders spannend, wenn Du bereits Ruff und uv verwendest und einen sehr schnellen Checker ausprobieren möchtest.
Da sich das Tool noch stark entwickelt, würde ich für Einsteiger weiterhin mypy oder Pyright als sichere Basis sehen und ty ergänzend beobachten oder testweise verwenden.
Pyrefly
Pyrefly ist ein Type Checker und Language Server von Meta.
Typischer Aufruf:
pyrefly check
Oder mit uv:
uvx pyrefly check
Pyrefly legt viel Wert auf Geschwindigkeit, IDE-Integration und breite Typing-Abdeckung.
Seit Version 1.0 ist Pyrefly deutlich interessanter für echte Projekte. Trotzdem gilt auch hier: Wenn Du gerade erst einsteigst, ist mypy oder Pyright meist der einfachere Start, weil Du dazu mehr bestehende Beispiele und Erfahrungswerte findest.
Checker in der CI ausführen
Ein Type Checker bringt besonders viel, wenn er regelmäßig läuft.
Lokal:
uv run mypy .
Oder:
uv run pyright
Oder:
uv run ty check
Oder:
uv run pyrefly check
In der CI könnte ein Projekt zum Beispiel ausführen:
uv sync --locked
uv run ruff check .
uv run ruff format --check .
uv run pytest
uv run mypy .
Dann werden Stil, Formatierung, Tests und Typen bei jedem Push oder Pull Request geprüft.
Die Reihenfolge ist Geschmackssache. Wichtig ist, dass der Type Checker nicht nur einmal lokal läuft und danach vergessen wird.
Typen und Tests ergänzen sich
Type Hints ersetzen keine Tests.
Ein Type Checker kann finden:
erleide_schaden("viel", 3)
Er prüft aber nicht automatisch, ob die Schadensregel fachlich stimmt.
Diese Funktion ist typkorrekt:
def erleide_schaden(hp: int, menge: int) -> int:
return hp + menge
Der Type Checker ist zufrieden: Integer hinein, Integer hinaus.
Fachlich ist die Funktion falsch, weil Schaden HP reduzieren sollte.
Dafür brauchst Du Tests:
def test_schaden_reduziert_hp():
assert erleide_schaden(100, 30) == 70
Type Hints prüfen Formen und Verträge. Tests prüfen Verhalten.
Beides zusammen ist stärker als eins allein.
Type Hints und Runtime-Validierung
Manche Daten kommen aus unsicheren Quellen:
- JSON-Dateien,
- HTTP-Requests,
- Benutzereingaben,
- Umgebungsvariablen,
- Datenbanken,
- CLI-Argumente.
Dort reicht ein Type Hint nicht.
Beispiel:
def lade_spielstand(daten: dict[str, object]) -> Spielstand:
...
Der Type Hint sagt nur, was die Funktion erwartet. Er garantiert nicht, dass die JSON-Datei wirklich diese Struktur hat.
Du musst weiterhin prüfen:
if not isinstance(daten.get("spieler"), dict):
raise SpielstandError("Ungültiger Spielstand.")
Oder ein Validierungswerkzeug verwenden.
Wichtige Regel:
Type Hints sind für statische Prüfung. Untrusted Input braucht Laufzeitvalidierung.
Schrittweise typisieren
Ein bestehendes Projekt komplett zu typisieren, kann überwältigend sein.
Besser:
- Neue Funktionen direkt annotieren.
- Kleine reine Funktionen zuerst typisieren.
- Öffentliche Schnittstellen annotieren.
- Datenmodelle und Dataclasses annotieren.
- Unklare Stellen mit Tests absichern.
Anynur bewusst einsetzen.- Strictness langsam erhöhen.
Für den Dungeon könntest Du starten mit:
def teile_befehl(eingabe: str) -> tuple[str, str]:
...
Dann:
def frage_ganzzahl(text: str, minimum: int | None = None) -> int:
...
Dann Klassen:
class Spieler:
name: str
hp: int
max_hp: int
gold: int
So wächst die Typabdeckung Schritt für Schritt.
Typische Stolperfallen
-
Type Hints als Laufzeitprüfung verstehen: Python erzwingt sie normalerweise nicht automatisch.
-
Zu früh zu streng werden: Ein großes untypisiertes Projekt sofort mit
strict = truezu prüfen, erzeugt oft zu viele Meldungen auf einmal. -
Überall
Anyverwenden: Das beruhigt den Checker, entfernt aber den Nutzen. -
cast(...)als echte Prüfung verstehen:castprüft zur Laufzeit nichts. -
Mutable Defaults falsch typisieren: Eine Annotation behebt nicht das Problem gemeinsam geteilter Listen.
python
# falsch
def __init__(self, inventar: list[str] = []):
...
Besser:
python
def __init__(self, inventar: list[str] | None = None):
self.inventar = list(inventar) if inventar is not None else []
-
Optionalfalsch lesen:str | Nonebedeutet nicht „optional als Argument“, sondern „Wert kann ein String oder None sein“. -
Zu enge Typen verwenden: Wenn eine Funktion nur iteriert, ist
Iterable[str]oft besser alslist[str]. -
Zu breite Typen verwenden:
objectoderAnyüberall macht die Signaturen wenig hilfreich. -
Type Aliases mit echten neuen Typen verwechseln:
type RaumId = strbleibt für die Laufzeit ein String-Konzept, kein neuer Datentyp. -
Checker-Warnungen blind wegdrücken:
# type: ignoresollte die Ausnahme sein und möglichst begründet werden. -
Tests durch Typen ersetzen wollen: Typen prüfen nicht die fachliche Logik.
-
Drittbibliotheken ohne Typinformationen unterschätzen: Manche Libraries sind gut typisiert, andere nicht. Das beeinflusst, wie viel ein Checker sehen kann.
Kompakte Übersicht
| Ziel | Syntax |
|---|---|
| Parameter annotieren | name: str |
| Rückgabewert annotieren | -> int |
| kein Rückgabewert | -> None |
| Liste von Strings | list[str] |
| Dictionary String zu Integer | dict[str, int] |
| Set von Raum-IDs | set[str] |
| Tupel aus zwei Integern | tuple[int, int] |
| beliebig langes String-Tupel | tuple[str, ...] |
| kann fehlen | Raum \| None |
| mehrere Typen | int \| str |
| Type Alias | type RaumId = str |
| feste Werte | Literal["normal", "schwer"] |
| Funktion als Argument | Callable[[str], int] |
| Verhalten statt Klasse | Protocol |
| bewusst dynamisch | Any |
| eigene Klasse zurückgeben | Self |
Übungen
1. Eine Funktion annotieren
Annotiere diese Funktion:
def heile(hp, menge, max_hp):
return min(hp + menge, max_hp)
Lösung
def heile(hp: int, menge: int, max_hp: int) -> int:
return min(hp + menge, max_hp)
2. Collections annotieren
Annotiere diese Variablen:
inventar = ["Fackel", "Brot"]
leben = {"Karl": 100, "Lara": 80}
besucht = {"halle", "krypta"}
position = (3, 7)
Lösung
inventar: list[str] = ["Fackel", "Brot"]
leben: dict[str, int] = {"Karl": 100, "Lara": 80}
besucht: set[str] = {"halle", "krypta"}
position: tuple[int, int] = (3, 7)
3. None korrekt ausdrücken
Annotiere eine Funktion finde_name(...), die entweder einen String oder
None zurückgibt.
Lösung
def finde_name(id_: int) -> str | None:
if id_ == 1:
return "Karl"
return None
4. Einen Type Alias anlegen
Lege einen Alias RaumId für str an und verwende ihn in einer Funktion.
Lösung
type RaumId = str
def bewege(ort: RaumId, ziel: RaumId) -> RaumId:
return ziel
RaumId = str
5. mypy ausführen
Installiere mypy als Development Dependency und prüfe das Projekt.
Lösung
Mit uv:uv add --dev mypy
uv run mypy .
python -m pip install mypy
python -m mypy .
6. Einen Type-Fehler erzeugen
Speichere diese Datei als typing_demo.py:
def erleide_schaden(hp: int, menge: int) -> int:
return max(hp - menge, 0)
erleide_schaden("viel", 3)
Lass einen Type Checker darüber laufen.
Lösung
Mit mypy:python -m mypy typing_demo.py
uv run mypy typing_demo.py
Vertiefung
- Python lernen, Teil 11: Werkzeugkasten und Ausblick
- pytest von Null
- Vom Skript zum echten CLI
- Dataclasses: Klassen ohne Boilerplate
- uv: pip und venv in schnell
Fazit
Type Hints machen Python nicht zu Java, Rust oder TypeScript.
Sie nehmen Python nicht die Dynamik. Sie geben Dir aber ein zusätzliches Sicherheitsnetz.
Eine Signatur wie:
def lade_spielstand(pfad: Path) -> Spielstand:
...
ist Dokumentation, Editorhilfe und Prüfgrundlage zugleich.
Mit einem Type Checker wie mypy oder Pyright werden daraus echte Warnungen vor dem Programmstart. Neuere Werkzeuge wie ty und Pyrefly zeigen, dass sich das Python-Typing-Ökosystem weiter stark bewegt.
Der beste Einstieg ist klein:
- neue Funktionen annotieren,
- zentrale Datenmodelle typisieren,
- mypy oder Pyright lokal laufen lassen,
- den Checker später in die CI aufnehmen,
- Tests weiterhin für fachliches Verhalten schreiben.
Dann helfen Type Hints, ohne dass sie zur Pflichtübung werden.
0 Kommentare
Noch keine Kommentare. Sei der/die Erste!
Anmelden um einen Kommentar zu hinterlassen.