Type Hints in Python und Überprüfung mit mypy
1. Übersicht
Python ist eine dynamisch typisierte Sprache – das bedeutet, du brauchst Datentypen nicht im Voraus zu deklarieren. Das ist flexibel, kann aber in größeren Projekten zu Laufzeitfehlern führen. Type Hints (PEP 484) ermöglichen dir, Datentypen zu annotieren, ohne das Laufzeitverhalten zu verändern. Während mypy diese Annotationen liest und Typfehler beim Schreiben des Codes erkennt.
Nach diesem Artikel wirst du wissen, wie du Type Hints für Variablen, Funktionen und Klassen schreibst, wie du Optional, Union, TypedDict und Generics nutzt und wie du mypy in deinen Code-Review-Prozess integrierst.
2. Type Hints in den Grundlagen
2.1. Typen für Variablen und Funktionen deklarieren
def calculate_total(price: float, quantity: int) -> float:
return price * quantity
customer_name: str = "Nguyễn Văn A"
total: float = calculate_total(price=15.5, quantity=3)
print(total)
price: float,quantity: intsind die Typen der Eingabeparameter.-> floatist der Typ des Rückgabewerts.customer_name: strist eine Annotation für die Variable und hilft der IDE bei Vorschlägen und mypy bei der Überprüfung.
46.5
Hinweis: Type Hints erzwingen KEINE Typkonvertierung zur Laufzeit. Wenn du
calculate_total("10", 2)aufrufst, läuft Python normal, da der Interpreter die Annotation ignoriert – nur Tools wie mypy melden Fehler.
2.2. mypy installieren
pip install mypy
Überprüfe eine Datei:
mypy order_service.py
Wenn keine Fehler vorhanden sind, gibt mypy aus:
Success: no issues found in 1 source file
3. Optional und Union für Werte, die None sein können
Viele Funktionen geben None zurück, wenn Daten nicht gefunden werden. Nutze Optional, um dies explizit auszudrücken.
from typing import Optional
class Customer:
def __init__(self, customer_id: int, email: str) -> None:
self.customer_id = customer_id
self.email = email
def find_customer(customer_id: int) -> Optional[Customer]:
database = {1: Customer(1, "a@example.com")}
return database.get(customer_id)
customer = find_customer(2)
if customer is not None:
print(customer.email)
Optional[Customer]entsprichtUnion[Customer, None].- Wenn du
customer.emailaufrufst, ohne vorher aufNonezu prüfen, meldet mypy den FehlerItem "None" of "Optional[Customer]" has no attribute "email".
from typing import Union
def format_id(value: Union[int, str]) -> str:
return str(value).zfill(6)
Union[int, str] erlaubt dem Parameter, entweder int oder str zu sein. Ab Python 3.10 kannst du kompakter schreiben: int | str statt Union[int, str] und Customer | None statt Optional[Customer].
4. Typen für Collections: list, dict, tuple
from typing import Dict, List, Tuple
def summarize_orders(order_totals: List[float]) -> Tuple[float, int]:
return sum(order_totals), len(order_totals)
def build_price_table(items: Dict[str, float]) -> Dict[str, float]:
return {name: price * 1.1 for name, price in items.items()}
List[float]bedeutet eine Liste von Gleitkommazahlen.Tuple[float, int]bedeutet ein Tupel mit genau 2 Elementen: zuerstfloat, dannint.Dict[str, float]ist ein Dictionary mit Schlüsseln vom Typstrund Werten vom Typfloat.
Hinweis: Ab Python 3.9 kannst du direkt
list[float],dict[str, float]verwenden, ohne austypingzu importieren.typing.List/typing.Dictnutzt du hauptsächlich für Kompatibilität mit Python 3.8 und älter.
5. TypedDict für strukturierte Dictionary-Daten
Wenn du mit JSON oder Dictionaries mit festem Schema arbeitest, hilft TypedDict mypy, jeden Schlüssel zu überprüfen.
from typing import TypedDict
class OrderPayload(TypedDict):
order_id: int
customer_email: str
total: float
def create_order(payload: OrderPayload) -> str:
return f"Order #{payload['order_id']} - {payload['customer_email']} - {payload['total']}"
payload: OrderPayload = {
"order_id": 101,
"customer_email": "b@example.com",
"total": 250.0,
}
print(create_order(payload))
Wenn du einen Schlüssel vergisst (total) oder ihn falsch schreibst ("totl"), meldet mypy:
error: Missing key "total" for TypedDict "OrderPayload"
Das ist ein großer Vorteil gegenüber Dict[str, Any] – du behältst die Kontrolle über die Datenstruktur, während du zur Laufzeit immer noch ein normales Python-Dictionary verwendest.
6. Generics mit TypeVar
Wenn du Funktionen oder Klassen schreibst, die für verschiedene Datentypen gelten, nutze TypeVar, um die Beziehung zwischen Ein- und Ausgabetyp beizubehalten.
from typing import TypeVar, List
T = TypeVar("T")
def first_item(items: List[T]) -> T:
return items[0]
first_name: str = first_item(["Hùng", "Lan"])
first_number: int = first_item([10, 20, 30])
TypeVar("T")erstellt einen Typ-Platzhalter.- Wenn du
first_item(["Hùng", "Lan"])aufrufst, leitet mypy her, dassT = str, also ist der Rückgabetypstr. - Wenn du mit
List[int]aufrufst, istT = int. So weiß mypy genau, welcher Typ zurückgegeben wird – anstatt nur allgemeinAnyzu sagen.
7. Klassen und Protocol statt Interface
Python hat keine Interfaces wie Java, aber Protocol (PEP 544) ermöglicht dir Structural Typing – es prüft “hat die richtige Methode”, statt dass du erben musst.
from typing import Protocol
class PaymentGateway(Protocol):
def charge(self, amount: float) -> bool:
...
class VnPayGateway:
def charge(self, amount: float) -> bool:
print(f"Charging {amount} via VNPay")
return True
def process_payment(gateway: PaymentGateway, amount: float) -> bool:
return gateway.charge(amount)
process_payment(VnPayGateway(), 199000.0)
VnPayGateway muss PaymentGateway nicht erben – es reicht, dass es die Methode charge(self, amount: float) -> bool hat, und mypy akzeptiert es als PaymentGateway. Das ist sehr „Pythonisch”: du behältst Duck Typing, aber mit Static Checking.
8. mypy für dein Projekt konfigurieren
Füge mypy in requirements-dev.txt hinzu:
mypy==1.11.2
Erstelle eine Konfigurationsdatei mypy.ini im Projektroot:
[mypy]
python_version = 3.12
disallow_untyped_defs = True
warn_return_any = True
warn_unused_ignores = True
ignore_missing_imports = True
disallow_untyped_defs = Trueerzwingt Type Hints für alle Funktionen, um nichts zu übersehen.warn_return_any = Truewarnt, wenn eine Funktion stillschweigendAnyzurückgibt.ignore_missing_imports = Trueist nützlich, wenn du Third-Party-Bibliotheken nutzt, die noch keine Type Stubs haben.
Überprüfe das gesamte Projekt:
mypy src/
9. Tests für Verhalten mit Type Hints schreiben
Type Hints ersetzen keine Tests, aber machen Tests klarer bezüglich Ein- und Ausgabetypen. Nutze pytest:
<!-- requirements-dev.txt in äquivalenter Form, nur zur Veranschaulichung der Abhängigkeitsstruktur -->
pytest==8.3.2
from order_service import calculate_total
def test_calculate_total_returns_correct_sum() -> None:
result = calculate_total(price=20.0, quantity=5)
assert result == 100.0
def test_calculate_total_with_zero_quantity() -> None:
result = calculate_total(price=50.0, quantity=0)
assert result == 0.0
Tests ausführen:
pytest -v
test_order_service.py::test_calculate_total_returns_correct_sum PASSED
test_order_service.py::test_calculate_total_with_zero_quantity PASSED
Du kannst beide Schritte in CI kombinieren: Führe zuerst mypy aus, um Typfehler zu fangen, dann pytest, um die Business-Logik zu überprüfen. Die beiden Tools ergänzen sich – mypy fängt „falscher Datentyp”, pytest fängt „falsche Logik”.
10. Häufige Fehler und wann du mypy nicht brauchst
- Type Hints vergessen zu aktualisieren, wenn die Logik sich ändert: Beispiel – eine Funktion geht von
Optional[Customer]zu nurCustomerüber, aber du vergisst die Annotation zu ändern – mypy wird keinen Fehler melden, wenn der Code noch dem älteren, breiteren Typ entspricht, weshalb die Information veraltet ist. Anyüberall verwenden:def process(data: Any) -> Anylässt mypy nicht überprüfen, es ist wie Static Typing zu deaktivieren.- Falsche Annotationen für Third-Party-Bibliotheken ohne Stubs: Viele alte Packages haben keine Type Stubs, mypy meldet
error: Cannot find implementation or library stub. Du löst das mitignore_missing_importsoder installiersttypes-<package>, wenn es verfügbar ist. - Wann du mypy NICHT streng anwenden solltest: Bei kleinen Skripten, einmaligen Scripts oder schnellen Prototypen – Type Hints zu erzwingen könnte das Schreiben verlangsamen ohne proportionalen Vorteil.
Hinweis: mypy macht nur statische Analyse des Source Codes, es führt das Programm nicht aus. Es kann Fehler nicht erkennen, die von Laufzeitdaten abhängen, zum Beispiel ob Werte aus Konfigurationsdateien oder externen APIs dem deklarierten Typ entsprechen.
11. Fazit
Type Hints machen deinen Python-Code lesbarer, wartbarer und reduzieren Fehler durch Typverwechslungen. mypy verwandelt diese Annotationen in ein echtes Überprüfungswerkzeug statt nur einer Dokumentation. Du solltest damit bei öffentlichen Funktionen und wichtigen Modulen anfangen und es allmählich auf das ganze Projekt ausweiten mit disallow_untyped_defs. mypy in CI zu integrieren zusammen mit pytest ist der praktischste Weg – du behältst Pythons Flexibilität, hast aber die Sicherheit von statischer Typisierung.