Mypy ist ein statischer Typprüfer („Type Checker“) für Python. Er hilft dir, Fehler schon beim Schreiben bzw. vor dem Ausführen des Codes zu finden – ähnlich wie ein Compiler in streng typisierten Sprachen – ohne dass du Python als dynamische Sprache „aufgibst“. Ich gehe Schritt für Schritt durch: 1. Grundidee (für Einsteiger verständlich) 2. Kurzer praktischer Einstieg (Installation, erste Checks, einfache Beispiele) 3. Welche Probleme mypy löst 4. Abgrenzung zu ähnlichen / verwandten Tools 5. Herausforderungen und typische Stolpersteine 6. Praxisnahe Beispiele und Patterns --- ## 1. Grundidee: Was macht mypy? Python ist dynamisch typisiert: Variablen haben zur Laufzeit Typen, aber der Interpreter prüft sie nicht im Voraus. Viele Fehler sieht man erst, wenn der entsprechende Code ausgeführt wird. Mypy ändert daran nichts zur Laufzeit – aber es analysiert deinen Code **statisch** (also ohne ihn auszuführen) und prüft, ob die verwendeten **Typannotationen** konsistent sind. ### Typannotationen – ein Beispiel Ohne Typen: ```python def add(a, b): return a + b ``` Das ist legal, aber du kannst aus dem Code nicht erkennen, ob `a` und `b` Zahlen, Strings oder etwas anderes sein sollen. Python lässt vieles zu: ```python print(add(1, 2)) # 3 print(add("a", "b")) # "ab" print(add(1, "b")) # TypeError zur Laufzeit ``` Mit Typannotationen: ```python def add(a: int, b: int) -> int: return a + b ``` Damit sagst du: `add` nimmt zwei `int` und gibt einen `int` zurück. Mypy überprüft jetzt: ```bash mypy mein_code.py ``` und meldet z.B.: ```text mein_code.py:10: error: Argument 2 to "add" has incompatible type "str"; expected "int" ``` – wenn du irgendwo `add(1, "b")` aufrufst. **Grundidee**: Du schreibst „Verträge“ (Typen) in deinen Code, und mypy überprüft, ob du dich überall daran hältst. Das verbessert Lesbarkeit, Robustheit und macht refactoring sicherer. --- ## 2. Kurzer praktischer Einstieg ### Installation ```bash pip install mypy ``` (Je nach Setup evtl. in einer virtuellen Umgebung.) ### Minimalbeispiel `calculator.py`: ```python def add(a: int, b: int) -> int: return a + b def main() -> None: x = add(1, 2) y = add("a", "b") # Fehler print(x, y) ``` Mypy ausführen: ```bash mypy calculator.py ``` Ausgabe: ```text calculator.py:6: error: Argument 1 to "add" has incompatible type "str"; expected "int" Found 1 error in 1 file (checked 1 source file) ``` Obwohl Python den Code ausführen würde (und bei `add("a", "b")` sogar ein „korrektes“ Ergebnis liefern würde: `"ab"`), sagt mypy: Du hast gegen deinen eigenen Typvertrag verstoßen. ### Gradual Typing Du musst nicht alles von Anfang an typisieren. Du kannst Schritt für Schritt anfangen: ```python def add(a, b): # keine Typen hier return a + b def use_add() -> int: result = add(1, 2) # mypy lässt das oft durchgehen (je nach Konfiguration) return result ``` Mypy arbeitet „gradual“: - Ungetypte Bereiche werden als `Any` betrachtet (unsicher, aber flexibel). - Getypte Bereiche werden überprüft. - Du kannst nach und nach mehr Typen hinzufügen und die Strenge erhöhen. --- ## 3. Welche Probleme werden durch mypy gelöst? ### 3.1. Typbezogene Fehler früh erkennen Typische Klassen von Bugs: 1. **Falsche Argumenttypen**: ```python def send_email(to: str, subject: str, body: str) -> None: ... send_email(["user@example.com"], "Hi", "Text") # Bug: Liste statt String ``` Mypy: ```text error: Argument 1 to "send_email" has incompatible type "List[str]"; expected "str" ``` 2. **Verfügbare Attribute/Methoden**: ```python def greet(name: str) -> None: print(name.upper()) user_name: int = 42 greet(user_name) ``` Mypy: ```text error: Argument 1 to "greet" has incompatible type "int"; expected "str" ``` 3. **Optionale Werte vergessen zu prüfen** (`None`): ```python from typing import Optional def get_user_name(user_id: int) -> Optional[str]: ... def print_name(user_id: int) -> None: name = get_user_name(user_id) print(name.upper()) # Bug: name kann None sein! ``` Mypy: ```text error: Item "None" of "Optional[str]" has no attribute "upper" ``` Du wirst gezwungen, zuerst auf `None` zu prüfen: ```python def print_name(user_id: int) -> None: name = get_user_name(user_id) if name is None: print("User not found") return print(name.upper()) # jetzt ok ``` ### 3.2. Sicherere Refactorings Wenn du Funktionensignaturen änderst, Parameter umbenennst oder Rückgabetypen anpasst, kann mypy dir helfen, alle Stellen zu finden, die du anpassen musst. Beispiel: ```python # vorher def get_price(product_id: int) -> float: ... # nachher def get_price(product_id: int) -> int: # Rückgabetyp geändert! ... ``` Wenn irgendwo angenommen wird, dass `float` zurückkommt: ```python price_cents: float = get_price(123) # jetzt inkonsistent ``` meldet mypy das. Das verringert das Risiko von subtilen Bugs nach Refactorings. ### 3.3. Bessere Dokumentation & IDE-Unterstützung Typannotationen sind lebende Dokumentation: ```python def load_config(path: str) -> dict[str, str]: ... ``` Du siehst sofort, was die Funktion erwartet und liefert – ohne lange Kommentare. IDEs nutzen die Typen für: - Autovervollständigung - Inlay Hints - Navigation („go to definition“) - Inline-Fehlermeldungen --- ## 4. Abgrenzung zu ähnlichen / verwandten Tools ### 4.1. Mypy vs. Linter (z.B. flake8, pylint) **Linter** prüfen v.a.: - Stil (PEP 8) - potenziell problematische Patterns (unbenutzte Variablen, Schatten von Builtins, zu komplexe Funktionen) - gewisse Logikfehler (z.B. nie erreichte Codezweige) **Mypy** fokussiert auf **Typkonsistenz**: - Stimmen die deklarierten Typen mit den tatsächlichen Verwendungen überein? - Können bestimmte Codezweige überhaupt erreicht werden, wenn Typen berücksichtigt werden? - Sind Operationen auf bestimmten Typen erlaubt? Beispiel: ```python x = [] x.append(1) x.append("a") ``` Linter: meistens kein Problem. Mypy (je nach Typinferenz) könnte sagen: ```text List item 1 has incompatible type "str"; expected "int" ``` Fazit: Linter und mypy ergänzen sich – sie ersetzen sich nicht. ### 4.2. Mypy vs. Testframeworks (pytest, unittest) **Tests**: - prüfen Laufzeitverhalten für konkrete Eingaben. - stellen sicher, dass Funktionen das tun, was fachlich / funktional gewünscht ist. **Mypy**: - prüft nur Typkonsistenz – keine fachliche Korrektheit. - findet z.B. nicht, ob du die falsche mathematische Formel verwendest, solange die Typen passen. Beispiel: ```python def calculate_discount(price: float) -> float: return price * 2 # fachlich falsch, aber typgerecht ``` Mypy ist zufrieden. Ein Unit-Test würde diesen Fehler finden. Fazit: Mypy ergänzt Tests, ersetzt sie aber nicht. ### 4.3. Mypy vs. andere Typechecker (Pyright, Pyre, pytype) Es gibt mehrere Typchecker für Python: - **mypy** – der „Klassiker“, in Python geschrieben, von vielen Projekten verwendet. - **pyright** – sehr schneller Typechecker (Microsoft), in TypeScript geschrieben. - **pyre** – von Meta (Facebook), mit Fokus auf große Codebasen. - **pytype** – von Google. Sie verfolgen alle eine ähnliche Idee: statische Typprüfung für Python. Unterschiede gibt es bei: - Performance - Genauigkeit / Strenge in bestimmten Bereichen - Tooling-Integration (z.B. VS Code nutzt intern Pyright) Für den Einstieg ist mypy völlig ausreichend und weit verbreitet. ### 4.4. Mypy vs. Laufzeit-Typprüfung (pydantic, marshmallow) **pydantic** & Co.: - validieren und konvertieren Daten zur **Laufzeit** (z.B. JSON-Input in API). - werfen Exceptions, wenn Daten nicht passen. - nutzen Typannotationen als Basis, sind aber nicht auf Compile-/Check-Zeit beschränkt. **Mypy**: - prüft nur zur Analysezeit, ändert das Laufzeitverhalten nicht. - „merkt nicht“, ob zur Laufzeit echte Validierung stattfindet. Beispiel mit pydantic: ```python from pydantic import BaseModel class User(BaseModel): id: int name: str user = User(id="123", name="Alice") # zur Laufzeit wird "123" zu int geparst ``` Mypy würde melden: ```text Argument "id" to "User" has incompatible type "str"; expected "int" ``` – obwohl pydantic das zur Laufzeit akzeptiert und konvertiert. Hier musst du entscheiden, ob du dich eher am statischen Vertrag (Typ) oder am dynamischen Verhalten orientieren willst. --- ## 5. Herausforderungen und typische Stolpersteine ### 5.1. Legacy Code ohne Typen In bestehendem Code fehlen oft Typannotationen, und vieles ist dynamisch. Strategien: - Zuerst nur neue Modules/Funktionen typisieren. - Mypy mit „lockereren“ Einstellungen starten. - Langsam „strictness“ erhöhen. Beispiel-Konfiguration (`mypy.ini`): ```ini [mypy] python_version = 3.11 ignore_missing_imports = True disallow_untyped_defs = False disallow_incomplete_defs = False ``` Später kannst du verschärfen: ```ini disallow_untyped_defs = True disallow_incomplete_defs = True warn_unused_ignores = True strict_optional = True ``` ### 5.2. Dynamische Features von Python Dinge wie: - dynamisches Hinzufügen von Attributen - `setattr`, `getattr` - Metaklassen-Magie - Monkey-Patching sind schwer für statische Analyser. Beispiel: ```python class Dynamic: pass obj = Dynamic() obj.name = "Alice" # dynamisches Attribut print(obj.name) ``` Mypy weiß nicht, dass `name` existiert, und meldet: ```text error: "Dynamic" has no attribute "name" ``` Workarounds: - Attribut im Klassendefinitionskörper deklarieren: ```python class Dynamic: name: str ``` - oder `# type: ignore` an problematischen Stellen nutzen. ### 5.3. Komplexe Typen und Verbosität Generics, `Union`, `Optional`, `TypedDict`, `Protocol` etc. können komplex werden. Das kostet Einarbeitung. Beispiel für generische Funktion: ```python from typing import TypeVar, Iterable, List T = TypeVar("T") def first(items: Iterable[T]) -> T: for item in items: return item raise ValueError("Empty iterable") ``` Mypy hilft hier, allgemeingültige, typsichere Utilities zu schreiben, aber das Typ-System wird relativ mächtig (und gelegentlich inelegant). ### 5.4. False Positives und `# type: ignore` Manchmal **weißt du mehr** als mypy. Dann musst du mit mypy kommunizieren: ```python from typing import cast, Any def get_from_json(json_obj: dict[str, Any]) -> int: return cast(int, json_obj["value"]) ``` oder: ```python some_weird_library_call() # type: ignore[arg-type] ``` Zu viele `# type: ignore` können aber wieder die Sicherheit untergraben. Es lohnt sich, sie sparsam und begründet einzusetzen. ### 5.5. Performance bei großen Codebasen Für wirklich große Projekte kann mypy langsamer werden, vor allem bei vielen Imports und tiefen Typstrukturen. Es gibt Optionen wie `--incremental` und `dmypy` (Daemon-Modus), um das zu beschleunigen. --- ## 6. Praxisnahe Beispiele & Patterns ### 6.1. Basic Typannotationen ```python def greet(name: str, times: int = 1) -> None: for _ in range(times): print(f"Hello, {name}!") ``` Sammlungstypen: ```python from typing import List, Dict def total_length(names: List[str]) -> int: length = 0 for n in names: length += len(n) return length def invert_mapping(mapping: Dict[int, str]) -> Dict[str, int]: return {v: k for k, v in mapping.items()} ``` Ab Python 3.9 kannst du oft die Kurzform nutzen: ```python def total_length(names: list[str]) -> int: ... def invert_mapping(mapping: dict[int, str]) -> dict[str, int]: ... ``` ### 6.2. Optional und Union ```python from typing import Optional, Union def parse_int(value: str) -> Optional[int]: try: return int(value) except ValueError: return None def stringify(value: Union[int, float]) -> str: return f"{value:.2f}" ``` Aufruf: ```python result = parse_int("123") if result is not None: print(result + 1) ``` Mypy zwingt dich, mit dem `None`-Fall umzugehen. ### 6.3. Typen für Klassen ```python class User: def __init__(self, user_id: int, name: str) -> None: self.user_id = user_id self.name = name def greet(self) -> str: return f"Hello, {self.name}!" ``` ### 6.4. Dataclasses mit Typen ```python from dataclasses import dataclass @dataclass class Product: id: int name: str price_cents: int def apply_discount(product: Product, percent: float) -> Product: discount = int(product.price_cents * percent / 100) return Product( id=product.id, name=product.name, price_cents=product.price_cents - discount ) ``` Mypy prüft, ob du `Product` überall korrekt verwendest. ### 6.5. Typen für „Dictionary-Objekte“: TypedDict Wenn du viele Dictionaries mit immer der gleichen Struktur verwendest (z.B. JSON): ```python from typing import TypedDict class UserData(TypedDict): id: int name: str email: str def send_welcome_mail(user: UserData) -> None: print(f"Sending mail to {user['email']}") ``` Mypy prüft: - fehlen Keys? - stimmen die Typen der Werte? ### 6.6. Duck Typing mit Protocol Du willst „etwas, das wie eine Datei ist“, nicht unbedingt eine konkrete Klasse: ```python from typing import Protocol class Writer(Protocol): def write(self, data: str) -> int: ... def log_to(writer: Writer, message: str) -> None: writer.write(message + "\n") ``` Jede Klasse, die `write(self, str) -> int` implementiert, passt – ganz im Sinne von „Duck Typing“, aber statisch überprüfbar. ### 6.7. Mypy-Konfiguration in der Praxis `mypy.ini` im Projektverzeichnis: ```ini [mypy] python_version = 3.11 strict = True # aktiviert viele strenge Checks # Beispiele für häufige Optionen: # disallow_untyped_defs = True # disallow_incomplete_defs = True # no_implicit_optional = True # warn_unused_ignores = True # warn_return_any = True ``` Du kannst auch per Modulbereich Regeln setzen: ```ini [mypy-myproject.legacy.*] ignore_errors = True # Legacy-Code vorerst ignorieren [mypy-myproject.core.*] strict = True ``` --- ## Zusammenfassung - **Grundidee**: mypy ist ein statischer Typprüfer für Python. Du annotierst deinen Code mit Typen, und mypy überprüft, ob der Code diese Verträge einhält. - **Probleme, die gelöst werden**: - Frühes Finden von typbezogenen Fehlern - Sicherere Refactorings - Bessere Dokumentation und Tooling/IDE-Unterstützung - **Abgrenzung**: - Linter → Stil & allgemeine Codequalität; mypy → Typkonsistenz - Tests → fachliche Korrektheit zur Laufzeit; mypy → statische Typkonsistenz - Andere Typchecker (Pyright etc.) verfolgen die gleiche Grundidee - Runtime-Validatoren (pydantic) prüfen zur Laufzeit, mypy nur zur Analysezeit - **Herausforderungen**: - Einstieg in bestehende, dynamische Codebasen - Dynamische Python-Features sind schwer statisch zu erfassen - Komplexe Typen können verbose sein - Manchmal sind `cast` und `# type: ignore` nötig Wenn du magst, kann ich dir als nächsten Schritt ein kleines Beispielprojekt skizzieren (z.B. eine Mini-API oder ein CLI-Tool) und zeigen, wie man mypy dort konkret einführt und schrittweise verschärft.