init at work

This commit is contained in:
Mathias Schneider
2026-03-27 12:58:01 +01:00
parent 352c352056
commit 8d40597732
60 changed files with 16498 additions and 0 deletions
Executable
+640
View File
@@ -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.