640 lines
15 KiB
Markdown
Executable File
640 lines
15 KiB
Markdown
Executable File
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. |