Python Type Hints
Python Type Hints erklärt: Variablen, Funktionen und Klassen annotieren, das typing-Modul verwenden und Typen mit mypy prüfen.
Type Hints ermöglichen es, erwartete Typinformationen an Variablen, Funktions-Parameter und Rückgabewerte anzuhängen. Python erzwingt sie zur Laufzeit nicht — sie sind Metadaten, die von Editoren, Lintern und Typprüfern wie mypy genutzt werden, um Fehler zu erkennen, bevor eine einzige Zeile ausgeführt wird.
Dieses Kapitel behandelt:
- Warum Type Hints wichtig sind und wann man sie einsetzt
- Annotieren von Variablen und Funktionen
- Eingebaute Typen und das
typing-Modul (List,Dict,Optional,Union,Tuple,Any,Callable) - Moderne Syntax (Python 3.10+)
- Annotieren von Klassen und
self - Generics und Typ-Aliase
- Statische Analyse mit mypy
- Häufige Fallstricke
Warum Type Hints?
Python ist dynamisch typisiert: Eine Variable kann einen Wert beliebigen Typs aufnehmen. Diese Flexibilität ist mächtig, macht aber große Codebasen schwerer navigierbar — man kann den Typ eines Funktionsarguments allein durch Lesen der Aufrufstelle nicht erkennen.
Type Hints lösen dieses Problem, ohne Pythons Dynamismus aufzugeben:
- Editoren zeigen Fehler sofort an. VS Code, PyCharm und andere unterstreichen Typ-Unstimmigkeiten direkt beim Tippen.
- Refactoring wird sicherer. Ändert man eine Funktionssignatur, zeigt der Typprüfer jede Aufrufstelle, die dadurch kaputtgeht.
- Code dokumentiert sich selbst.
def greet(name: str) -> strkommuniziert den Vertrag ohne Docstring. - Bibliotheken werden einfacher nutzbar. Typisierte Bibliotheken bieten Autovervollständigung für jedes Attribut und jede Methode.
Type Hints wurden in Python 3.5 durch PEP 484 eingeführt. Die Syntax wurde in jeder größeren Version seither verfeinert. Die folgenden Beispiele geben die minimale Python-Version an, ab der die jeweilige Syntax erstmals verfügbar war.
Variablen annotieren
Füge nach dem Variablennamen einen Doppelpunkt gefolgt vom Typ ein:
name: str = "Alice"
age: int = 30
price: float = 9.99
is_active: bool = TrueMan kann den Typ einer Variablen auch deklarieren, ohne ihr sofort einen Wert zuzuweisen. Dies nennt sich Vorwärtsdeklaration und ist innerhalb von Klassen oder auf Modulebene nützlich:
user_id: int # declared but not yet assigned
user_id = 42Typ-Annotationen auf Modulebene beeinflussen das Laufzeitverhalten nicht — sie werden im __annotations__-Dictionary des Moduls gespeichert, vom Interpreter aber ansonsten ignoriert.
Funktionen annotieren
Annotationen werden bei Parametern (nach dem Doppelpunkt) und beim Rückgabewert (nach -> vor dem abschließenden Doppelpunkt der Signatur) angegeben:
def add(a: int, b: int) -> int:
return a + b
def greet(name: str) -> str:
return f"Hello, {name}!"
def send_email(to: str, subject: str, body: str) -> None:
print(f"Sending '{subject}' to {to}")
result: int = add(3, 5)
message: str = greet("Alice")-> None bedeutet, dass die Funktion keinen sinnvollen Rückgabewert hat (sie gibt implizit None zurück). Das Weglassen der Rückgabeannotation ist ebenfalls gültig, aber ein explizites -> None macht die Absicht deutlicher.
Standardparameter
Standardwerte werden nach der Annotation angegeben:
def connect(host: str, port: int = 8080, secure: bool = False) -> None:
print(f"Connecting to {host}:{port} (secure={secure})")
connect("example.com") # uses defaults
connect("example.com", 443, True)*args und **kwargs
Annotiere den Elementtyp, nicht den Kollektionstyp:
def total(*prices: float) -> float:
return sum(prices)
def create_user(**fields: str) -> dict:
return fields
print(round(total(9.99, 4.50, 12.00), 2)) # 26.49
print(create_user(name="Bob", role="admin"))*prices: float bedeutet, dass jedes Positionsargument ein float ist; zur Laufzeit ist prices nach wie vor ein normales tuple aus Floats. Ebenso bedeutet **fields: str, dass der Wert jedes Schlüsselwortarguments ein str ist.
Das typing-Modul
Für alles, was über die grundlegenden eingebauten Typen hinausgeht, importiere aus dem typing-Modul (Python 3.5+). Ab Python 3.9 wurden viele typing-Typen direkt in die eingebauten Äquivalente übernommen (siehe Moderne Syntax weiter unten).
List, Tuple, Set, Dict
from typing import List, Tuple, Set, Dict
def first_names(users: List[str]) -> str:
return users[0] if users else ""
def dimensions() -> Tuple[int, int, int]:
return (1920, 1080, 32)
def unique_tags(items: List[str]) -> Set[str]:
return set(items)
def word_count(text: str) -> Dict[str, int]:
counts: Dict[str, int] = {}
for word in text.split():
counts[word] = counts.get(word, 0) + 1
return counts
print(first_names(["Alice", "Bob"])) # Alice
print(dimensions()) # (1920, 1080, 32)
print(unique_tags(["py", "web", "py"])) # {'py', 'web'}
print(word_count("one two one")) # {'one': 2, 'two': 1}Optional
Optional[X] ist eine Kurzschreibweise für Union[X, None]. Verwende es, wenn ein Wert fehlen kann:
from typing import Optional
def find_user(user_id: int) -> Optional[str]:
db = {1: "Alice", 2: "Bob"}
return db.get(user_id) # returns None if not found
name = find_user(1)
if name is not None:
print(name.upper()) # ALICE
missing = find_user(99)
print(missing) # NoneEin Typprüfer erkennt Optional[str] und weiß, dass vor dem Aufruf von String-Methoden auf das Ergebnis eine None-Prüfung erforderlich ist. Fehlt diese Prüfung, meldet er einen Fehler.
Union
Union[X, Y] bedeutet, dass der Wert entweder Typ X oder Typ Y haben kann:
from typing import Union
def stringify(value: Union[int, float, str]) -> str:
return str(value)
print(stringify(42)) # 42
print(stringify(3.14)) # 3.14
print(stringify("hi")) # hiUnion ist am nützlichsten, wenn eine Funktion tatsächlich mehrere nicht verwandte Typen akzeptiert. Wenn man Union[str, None] schreibt, sollte man stattdessen Optional[str] verwenden — das ist idiomatischer.
Callable
Callable[[ArgTypes...], ReturnType] annotiert eine Funktion, die als Argument übergeben wird:
from typing import Callable
def apply_twice(func: Callable[[int], int], value: int) -> int:
return func(func(value))
def double(n: int) -> int:
return n * 2
print(apply_twice(double, 3)) # 12Callable[[int], int] bedeutet: ein Callable, das ein int-Argument entgegennimmt und ein int zurückgibt. Wenn die Argumentliste komplex oder unbekannt ist, verwende Callable[..., ReturnType].
Any
Any ist ein spezieller Typ, der die Typprüfung für diesen Wert deaktiviert. Jeder Typ ist sowohl zu Any als auch von Any zuweisbar:
from typing import Any
def log(value: Any) -> None:
print(value)
log(42)
log("hello")
log([1, 2, 3])Verwende Any sparsam — es ist ein Notausgang, der genau den Schutz aufhebt, den Type Hints bieten. Es ist angebracht, wenn man mit nicht typisiertem Drittanbieter-Code arbeitet oder eine große Codebasis schrittweise migriert.
Moderne Syntax (Python 3.9+, 3.10+)
Eingebaute Generics (Python 3.9+)
Ab Python 3.9 kann man die eingebauten Typen direkt als Generics verwenden, ohne aus typing zu importieren:
# Python 3.9+
def word_count(text: str) -> dict[str, int]:
counts: dict[str, int] = {}
for word in text.split():
counts[word] = counts.get(word, 0) + 1
return counts
def first(items: list[int]) -> int | None:
return items[0] if items else None
print(word_count("cat dog cat")) # {'cat': 2, 'dog': 1}
print(first([10, 20, 30])) # 10
print(first([])) # NoneVerwende list[str] statt List[str], dict[str, int] statt Dict[str, int] usw.
X | Y-Union-Syntax (Python 3.10+)
Python 3.10 führte den |-Operator für Unions ein, der Union[X, Y] und Optional[X] ersetzt:
# Python 3.10+
def parse(value: str | int | None) -> str:
if value is None:
return "nothing"
return str(value)
print(parse("hello")) # hello
print(parse(42)) # 42
print(parse(None)) # nothingstr | None ist äquivalent zu Optional[str]. Diese Syntax ist klarer und leichter lesbar.
Klassen annotieren
Instanzattribute werden innerhalb von __init__ annotiert, und Methoden erhalten Rückgabeannotationen:
class BankAccount:
owner: str # class-level annotation (no default value)
balance: float
def __init__(self, owner: str, initial_balance: float = 0.0) -> None:
self.owner = owner
self.balance = initial_balance
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("Deposit amount must be positive.")
self.balance += amount
def withdraw(self, amount: float) -> bool:
if amount > self.balance:
return False
self.balance -= amount
return True
def __repr__(self) -> str:
return f"BankAccount(owner={self.owner!r}, balance={self.balance:.2f})"
account = BankAccount("Alice", 100.0)
account.deposit(50.0)
print(account.withdraw(30.0)) # True
print(account) # BankAccount(owner='Alice', balance=120.00)Die Annotation von self wird immer abgeleitet — man schreibt nie self: BankAccount. Der Rückgabetyp von __init__ ist immer None.
ClassVar
Mit ClassVar[T] (aus typing) markiert man ein Attribut, das zur Klasse gehört, nicht zu jeder Instanz:
from typing import ClassVar
class Config:
MAX_RETRIES: ClassVar[int] = 3
timeout: int
def __init__(self, timeout: int) -> None:
self.timeout = timeout
print(Config.MAX_RETRIES) # 3Ein Typprüfer warnt, wenn man versucht, ClassVar auf einer Instanz zu setzen — es ist dazu gedacht, auf Klassenebene geteilt zu werden.
Typ-Aliase
Ein Typ-Alias gibt einem langen oder komplexen Typ einen kürzeren, aussagekräftigeren Namen:
from typing import List, Tuple
# Simple alias
UserID = int
Filename = str
# Structured alias
Coordinates = Tuple[float, float]
Matrix = List[List[float]]
def distance(p1: Coordinates, p2: Coordinates) -> float:
return ((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) ** 0.5
print(distance((0.0, 0.0), (3.0, 4.0))) # 5.0Ab Python 3.12 verwendet man die type-Anweisung für explizite, inspizierbare Aliase:
# Python 3.12+
type Vector = list[float]
type Matrix = list[Vector]Generics mit TypeVar
TypeVar ermöglicht es, eine einzige Funktion zu schreiben, die mit beliebigen Typen funktioniert, während Typbeziehungen erhalten bleiben:
from typing import TypeVar, List
T = TypeVar("T")
def first_item(items: List[T]) -> T:
return items[0]
x: int = first_item([1, 2, 3]) # x is int
s: str = first_item(["a", "b"]) # s is strDer Typprüfer leitet aus dem Argument ab, was T ist, und gibt diese Information an den Rückgabetyp weiter. Ohne TypeVar müsste man Any zurückgeben und verlöre die Typsicherheit.
Man kann TypeVar auf eine Menge erlaubter Typen beschränken:
from typing import TypeVar
Numeric = TypeVar("Numeric", int, float)
def double(n: Numeric) -> Numeric:
return n * 2
print(double(4)) # 8 (int)
print(double(2.5)) # 5.0 (float)Statische Typprüfung mit mypy
mypy ist der am weitesten verbreitete statische Typprüfer für Python. Installation über pip:
pip install mypyDann auf eine Datei anwenden:
mypy my_script.pyBeispiel: Einen Fehler mit mypy finden
Speichere folgendes als demo.py:
def greet(name: str) -> str:
return f"Hello, {name}!"
result = greet(42) # passing int instead of str
print(result.upper())mypy demo.py meldet:
demo.py:4: error: Argument 1 to "greet" has incompatible type "int"; expected "str"
Found 1 error in 1 file (checked 1 source file)Python selbst führt den Code problemlos aus (f-Strings wandeln jeden Typ um), aber mypy hat die Diskrepanz erkannt, bevor man sie in der Produktion entdecken musste.
Nützliche mypy-Optionen
| Flag | Wirkung |
|---|---|
--strict | Alle optionalen Prüfungen aktivieren (empfohlen für neue Projekte) |
--ignore-missing-imports | Fehler wegen fehlender Drittanbieter-Stubs unterdrücken |
--check-untyped-defs | Auch Funktionen ohne Annotationen typprüfen |
--disallow-untyped-defs | Annotationen für alle Funktionsdefinitionen verlangen |
Eine mypy.ini (oder [tool.mypy] in pyproject.toml) hält die Konfiguration aus der Kommandozeile heraus:
[mypy]
strict = true
ignore_missing_imports = trueSchrittweise Typisierung
Man muss nicht alle Funktionen auf einmal annotieren. Python unterstützt schrittweise Typisierung (gradual typing): annotierter und nicht annotierter Code koexistieren problemlos. mypy überspringt nicht annotierte Funktionen standardmäßig (sofern nicht --check-untyped-defs gesetzt ist).
Ein praktischer Ansatz für eine bestehende Codebasis:
- Neuen Code von Beginn an mit Annotationen versehen.
- Zuerst die am häufigsten aufgerufenen oder fehleranfälligsten Funktionen annotieren.
--strictModul für Modul aktivieren, wenn die Abdeckung steigt.Anynur dort verwenden, wo eine Drittanbieter-Bibliothek nicht typisiert ist, und einen Kommentar hinzufügen, der erklärt warum.
Häufige Fallstricke
Vorwärtsreferenzen
Wenn ein Typ auf eine Klasse verweist, die später in derselben Datei definiert ist, den Namen in Anführungszeichen setzen, um ihn zu einem String (einer Vorwärtsreferenz) zu machen:
class Node:
def __init__(self, value: int, next: "Node | None" = None) -> None:
self.value = value
self.next = next
head = Node(1, Node(2))
print(head.value, head.next.value) # 1 2Ab Python 3.10+ kann man stattdessen from __future__ import annotations am Anfang der Datei hinzufügen. Das macht alle Annotationen zu lazy Strings und beseitigt den Bedarf an manuellem Quoting.
Annotationen zur Laufzeit
Standardmäßig werden Annotationen in Python 3.9 und früher sofort ausgewertet. Das bedeutet, dass eine Vorwärtsreferenz ohne Anführungszeichen einen NameError auslöst:
# Works (with quotes):
def clone(self: "MyClass") -> "MyClass": ...Mit from __future__ import annotations (Python 3.7+) werden alle Annotationen als Strings gespeichert und nur bei Inspektion ausgewertet — das löst das Problem der Vorwärtsreferenzen automatisch.
None vs. Optional
Ein häufiger Fehler ist, den Rückgabetyp als str zu annotieren, obwohl die Funktion tatsächlich None zurückgeben kann. Immer Optional[str] (oder str | None) verwenden, wenn None ein möglicher Rückgabewert ist:
from typing import Optional
# Wrong — mypy will flag callers that assume this is always str
def get_name(user_id: int) -> str:
if user_id == 0:
return None # type: ignore — this is the bug
# Correct
def get_name_safe(user_id: int) -> Optional[str]:
if user_id == 0:
return None
return "Alice"list vs. List (Versionskompatibilität)
Wenn der Code auf Python 3.8 oder früher läuft, muss man from typing import List verwenden und List[str] schreiben. Ab Python 3.9 funktioniert list[str] direkt. Wenn beide Versionen unterstützt werden sollen, entweder die typing-Importe verwenden oder from __future__ import annotations hinzufügen.
Kurzreferenz
| Annotation | Bedeutung |
|---|---|
x: int | Variable x ist ein Integer |
def f(a: str) -> bool | Parameter a ist str; Rückgabewert ist bool |
-> None | Funktion gibt keinen sinnvollen Wert zurück |
Optional[str] | str oder None |
Union[int, str] | int oder str |
list[int] / List[int] | Liste von Integern |
dict[str, int] / Dict[str, int] | Dict, das str auf int abbildet |
tuple[int, str] / Tuple[int, str] | Tupel aus (int, str) |
Callable[[int], str] | Funktion, die int nimmt und str zurückgibt |
Any | Beliebiger Typ (deaktiviert Prüfung) |
ClassVar[T] | Attribut auf Klassenebene |
TypeVar("T") | Generische Typvariable |
Verwandte Themen
- Python Functions — wo Typ-Annotationen für Parameter und Rückgabetypen stehen.
- Python Classes and Objects — zum Annotieren von
__init__, Methoden und Klassenattributen. - Python Dataclasses — Typ-Annotationen sind erforderlich, um Dataclass-Felder zu deklarieren.
- Python Abstract Classes — abstrakte Basisklassen funktionieren natürlich mit Type Hints.