add synced notes on IT know how
This commit is contained in:
+640
@@ -0,0 +1,640 @@
|
||||
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.
|
||||
Reference in New Issue
Block a user