W3docs

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) -> str kommuniziert 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 = True

Man 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 = 42

Typ-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)               # None

Ein 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"))    # hi

Union 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))   # 12

Callable[[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([]))                   # None

Verwende 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))      # nothing

str | 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)   # 3

Ein 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.0

Ab 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 str

Der 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 mypy

Dann auf eine Datei anwenden:

mypy my_script.py

Beispiel: 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

FlagWirkung
--strictAlle optionalen Prüfungen aktivieren (empfohlen für neue Projekte)
--ignore-missing-importsFehler wegen fehlender Drittanbieter-Stubs unterdrücken
--check-untyped-defsAuch Funktionen ohne Annotationen typprüfen
--disallow-untyped-defsAnnotationen 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 = true

Schrittweise 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:

  1. Neuen Code von Beginn an mit Annotationen versehen.
  2. Zuerst die am häufigsten aufgerufenen oder fehleranfälligsten Funktionen annotieren.
  3. --strict Modul für Modul aktivieren, wenn die Abdeckung steigt.
  4. Any nur 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 2

Ab 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

AnnotationBedeutung
x: intVariable x ist ein Integer
def f(a: str) -> boolParameter a ist str; Rückgabewert ist bool
-> NoneFunktion 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
AnyBeliebiger Typ (deaktiviert Prüfung)
ClassVar[T]Attribut auf Klassenebene
TypeVar("T")Generische Typvariable

Verwandte Themen

Übungen

Übung
What does Optional[str] mean in a Python type hint?
What does Optional[str] mean in a Python type hint?
Übung
Which annotation correctly types a function that accepts a list of integers and returns a single integer?
Which annotation correctly types a function that accepts a list of integers and returns a single integer?
Übung
What is the purpose of TypeVar in the typing module?
What is the purpose of TypeVar in the typing module?
Was this page helpful?