add synced notes on IT know how

This commit is contained in:
Mathias Schneider
2026-03-17 19:01:50 +01:00
parent b8797d5ca8
commit fc3aef4da8
31 changed files with 13162 additions and 1 deletions
+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.