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
+411
View File
@@ -0,0 +1,411 @@
Hier eine Einführung in „asynchron“ in der Python-Programmierung, für jemanden ohne Vorkenntnisse in diesem Bereich.
---
## 1. Intuitive Vorstellung: Was heißt „asynchron“?
Stell dir vor, du kochst:
- Du stellst Wasser auf den Herd (es braucht Zeit, bis es kocht).
- Während du wartest, schneidest du Gemüse, bereitest Sauce vor usw.
- Du **blockierst nicht** deine Zeit, indem du nur auf den Topf starrst.
**Asynchron** bedeutet in der Programmierung:
Dein Programm kann etwas starten, das länger dauert (z.B. eine Netzwerkabfrage), und während es darauf wartet, **andere Dinge erledigen**, statt „untätig“ zu blockieren.
---
## 2. Grundbegriffe: synchron vs. asynchron
### 2.1 Synchron (blockierend)
Synchroner, „klassischer“ Code:
- Befehl A wird ausgeführt.
- Erst wenn A fertig ist, wird B ausgeführt.
- Wenn A lange wartet (z.B. auf eine Antwort aus dem Internet), **steht das ganze Programm an dieser Stelle still**.
Beispiel (synchron, blockierend):
```python
import time
print("Starte")
time.sleep(5) # wartet 5 Sekunden – Programm ist blockiert
print("Fertig")
```
Während `time.sleep(5)` läuft, kann das Programm nichts anderes tun.
### 2.2 Asynchron (nicht-blockierend innerhalb eines Ablaufs)
Asynchroner Code versucht:
- Langsame Operationen (z.B. Netzwerk, Festplatte, Datenbank) so zu starten,
- und während sie „laufen“, andere Aufgaben zu bearbeiten.
In Python machst du das typischerweise mit `async` und `await`.
Sehr vereinfachtes Beispiel:
```python
import asyncio
async def aufgabe(name, dauer):
print(f"{name} gestartet")
await asyncio.sleep(dauer) # nicht-blockierend warten
print(f"{name} fertig")
async def main():
# Zwei Aufgaben (Tasks) gleichzeitig laufen lassen
task1 = asyncio.create_task(aufgabe("A", 2))
task2 = asyncio.create_task(aufgabe("B", 2))
await task1
await task2
asyncio.run(main())
```
Typischer Ablauf:
- „A gestartet“
- „B gestartet“
- (2 Sekunden vergehen)
- „A fertig“
- „B fertig“
Beide Aufgaben „warten gleichzeitig“, und die Zeit überlappt sich.
---
## 3. Wichtige Abgrenzungen: Begriffe, die oft durcheinandergehen
### 3.1 Nebenläufigkeit (Concurrency) vs. Parallelität
- **Nebenläufigkeit (Concurrency)**: Mehrere Aufgaben werden so organisiert, dass sie *scheinbar gleichzeitig* laufen, indem man schnell zwischen ihnen hin- und herschaltet.
- **Parallelität (Parallelism)**: Mehrere Aufgaben laufen *wirklich gleichzeitig* auf mehreren CPU-Kernen.
Asynchrones Programmieren in Python (`asyncio`) ist in erster Linie ein Werkzeug für **Nebenläufigkeit**, nicht zwingend für echte Parallelität.
### 3.2 Threads vs. Async
- **Threads**:
- Betriebssystem-Fäden (OS-Threads).
- Können an verschiedenen CPU-Kernen parallel laufen.
- Schwerer zu testen, zu debuggen (Race Conditions, Deadlocks).
- **Async (z.B. asyncio in Python)**:
- Läuft typischerweise in **einem** Thread.
- Nutzt einen **Event Loop**, um zwischen Aufgaben zu wechseln, wenn sie gerade warten.
- Sehr gut geeignet, wenn viele Aufgaben hauptsächlich **I/O-lastig** sind (Netzwerk, Dateien).
**Kurz:**
- Viele Netzwerk-Anfragen gleichzeitig? → Async kann ideal sein.
- Viel CPU-Berechnung (z.B. Bildverarbeitung)? → Threads oder Prozesse (Multiprocessing) sind oft sinnvoller.
### 3.3 Blocking vs. Non-blocking I/O
- **Blockierend**: „Lies aus dem Netzwerk“ – der Code bleibt stehen, bis Daten da sind.
- **Non-blockierend**: „Lies aus dem Netzwerk, aber wenn gerade nichts da ist, mach solange andere Aufgaben.“
Asynchrones Python nutzt non-blocking I/O und einen Event Loop, um viele solcher Operationen gleichzeitig zu verwalten.
---
## 4. Asynchron in Python konkret: `asyncio`, `async`, `await`
### 4.1 Historischer Kontext
- Vor Python 3.4 gab es `asyncio` nur als externes Paket.
- Ab Python 3.5 wurden die Schlüsselwörter `async` und `await` eingeführt und haben das Arbeiten mit Async deutlich angenehmer gemacht.
### 4.2 Zentrale Begriffe
- **Coroutine**: eine Funktion, die „angehalten“ und später fortgesetzt werden kann. In Python: definiert mit `async def`.
- **Event Loop**: eine Schleife, die:
- Aufgaben plant,
- sie laufen lässt, bis sie warten müssen (z.B. auf I/O),
- dann anderen Aufgaben CPU-Zeit gibt.
- **Task**: eine geplante Coroutine, die vom Event Loop verwaltet wird.
- **Future**: ein Platzhalter für ein Ergebnis, das noch nicht fertig ist.
### 4.3 Einfaches Beispiel: Event Loop und Coroutines
```python
import asyncio
async def hallo():
print("Hallo...")
await asyncio.sleep(1) # simuliert I/O-Wartezeit
print("...Welt!")
async def main():
await hallo()
asyncio.run(main())
```
- `hallo()` ist eine Coroutine.
- `await asyncio.sleep(1)` bedeutet: „warte 1 Sekunde, aber blockiere nicht den Event Loop“.
---
## 5. Welche Probleme werden durch asynchrones Programmieren gelöst?
### 5.1 Viele gleichzeitige I/O-Aufgaben
Typische Beispiele:
- Webserver, die viele gleichzeitige HTTP-Anfragen beantworten.
- Web-Scraper oder Clients, die viele HTTP-Anfragen an andere Server stellen.
- Chat-Server, WebSockets, Streaming.
- Programme, die gleichzeitig:
- Dateien lesen/schreiben,
- mit einer Datenbank kommunizieren,
- HTTP-Anfragen senden.
**Synchroner Ansatz**:
Jede Anfrage blockiert einen Thread/Prozess, solange sie auf Antwort wartet → sehr viele Threads/Prozesse nötig.
**Asynchroner Ansatz**:
Ein Event Loop verwaltet viele tausend Verbindungen in einem oder wenigen Threads, indem er immer dort weiterarbeitet, wo gerade Daten verfügbar sind.
### 5.2 Bessere Ressourcennutzung bei I/O-lastigen Programmen
Wenn dein Programm hauptsächlich:
- Daten lädt (HTTP, DB),
- auf Antworten wartet,
- nicht viel rechnet,
dann ist asynchroner Code oft **effizienter** (weniger Overhead, weniger Threads, bessere Skalierung).
### 5.3 Responsivere Anwendungen (z.B. GUIs)
In grafischen Anwendungen (oder auch CLI-Tools) willst du:
- Nicht, dass die Oberfläche „einfriert“, während eine Anfrage ans Internet läuft.
- Stattdessen nutzt du asynchrone oder nebenläufige Mechanismen, damit der Haupt-Thread weiterhin Eingaben entgegennimmt.
---
## 6. Praxisnahe Beispiele
### 6.1 Vergleich: synchron vs. asynchron HTTP-Anfragen
#### Synchron: nacheinander mit `requests`
```python
import requests
urls = [
"https://example.com",
"https://httpbin.org/delay/2",
"https://httpbin.org/delay/3",
]
def fetch(url):
print(f"Rufe {url} ab...")
response = requests.get(url)
print(f"{url}: Status {response.status_code}")
def main():
for url in urls:
fetch(url)
if __name__ == "__main__":
main()
```
- Jede Anfrage wartet, bis sie fertig ist.
- Gesamtzeit ≈ Summe aller Wartezeiten.
#### Asynchron: gleichzeitig mit `aiohttp` und `asyncio`
```python
import asyncio
import aiohttp
urls = [
"https://example.com",
"https://httpbin.org/delay/2",
"https://httpbin.org/delay/3",
]
async def fetch(session, url):
print(f"Rufe {url} ab...")
async with session.get(url) as response:
print(f"{url}: Status {response.status}")
async def main():
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
await asyncio.gather(*tasks) # starte alle gleichzeitig
if __name__ == "__main__":
asyncio.run(main())
```
- Alle Anfragen werden „gleichzeitig“ gestartet.
- Gesamtzeit ≈ maximale Einzeldauer, nicht Summe.
### 6.2 Viele „Schlaf-Aufgaben“ parallel (Simulation von I/O)
```python
import asyncio
import random
async def simulierte_io_aufgabe(n):
dauer = random.uniform(0.5, 2.0)
print(f"Aufgabe {n} startet, Dauer ~{dauer:.2f}s")
await asyncio.sleep(dauer)
print(f"Aufgabe {n} fertig")
async def main():
tasks = [simulierte_io_aufgabe(i) for i in range(5)]
await asyncio.gather(*tasks)
asyncio.run(main())
```
Output (ähnlich):
- Mehrere Aufgaben starten schnell hintereinander.
- Sie enden in anderer Reihenfolge, je nach Dauer.
- Die Gesamtzeit liegt in etwa bei der **längsten** Wartezeit, nicht bei der Summe aller.
---
## 7. Typische Herausforderungen und Stolperfallen
### 7.1 Denken in „async“ ist ungewohnt
Für Einsteiger:
- Man kann `await` **nur** in `async def`-Funktionen verwenden.
- Asynchrone Funktionen verhalten sich anders als normale:
```python
async def foo():
return 42
# Aufruf:
result = foo() # das ist KEINE 42, sondern eine Coroutine!
```
Du musst sie ausführen:
```python
import asyncio
async def foo():
return 42
async def main():
result = await foo()
print(result)
asyncio.run(main())
```
### 7.2 Blockierender Code in asynchronem Kontext
Problem:
- Du hast eine `async`-Funktion, benutzt darin aber eine **blockierende** Bibliothek (z.B. `requests`, `time.sleep`).
- Dann blockierst du trotzdem den Event Loop, obwohl du „async“ verwendest.
Beispiel (so besser nicht):
```python
import asyncio
import time
async def schlecht():
print("Blockiere Event Loop...")
time.sleep(5) # blockiert den Event Loop komplett!
print("Weiter geht's")
asyncio.run(schlecht())
```
Lösung:
- Entweder eine **asynchrone Alternative** verwenden (z.B. `aiohttp` statt `requests`).
- Oder blockierende Funktion in einem Thread/Prozess auslagern (z.B. `asyncio.to_thread`).
```python
import asyncio
import time
def blockierende_funktion():
time.sleep(5)
return "fertig"
async def gut():
print("Starte blockierende Funktion in Thread...")
result = await asyncio.to_thread(blockierende_funktion)
print("Ergebnis:", result)
asyncio.run(gut())
```
### 7.3 Debugging und Fehlersuche
- Fehler in asynchronen Programmen können schwerer nachzuvollziehen sein.
- Stack-Traces sehen anders aus, weil Coroutines, Tasks und der Event Loop beteiligt sind.
- Es kann passieren, dass Tasks „stillschweigend“ fehlschlagen, wenn man sie nicht korrekt awaited oder Fehler nicht abfängt.
### 7.4 Testen von asynchronem Code
- Unit-Tests brauchen meist auch einen Event Loop.
- Viele Testframeworks bieten dafür Mechanismen (`pytest` mit `pytest-asyncio`).
Beispiel mit `pytest-asyncio`:
```python
# test_example.py
import pytest
import asyncio
async def verdoppeln(x):
await asyncio.sleep(0.1)
return x * 2
@pytest.mark.asyncio
async def test_verdoppeln():
assert await verdoppeln(21) == 42
```
---
## 8. Wann lohnt sich asynchrones Programmieren (und wann nicht)?
**Sinnvoll bei:**
- Web-APIs, Microservices, Webserver.
- Chat-Server, WebSocket-Anwendungen.
- Web-Scraping vieler Seiten gleichzeitig.
- I/O-lastigen Programmen mit vielen Netzwerk- oder Datenbankzugriffen.
**Weniger sinnvoll bei:**
- Reinen CPU-lastigen Aufgaben (z.B. numerische Berechnungen, Bildverarbeitung).
- Hier helfen eher: mehrere Prozesse (`multiprocessing`) oder spezialisierte Bibliotheken (NumPy, Numba, etc.).
- Kleinen Scripts, die nur wenige, einfache Schritte nacheinander machen – da ist synchroner Code oft einfacher und ausreichend.
---
## 9. Zusammenfassung in einfachen Worten
- **Asynchron** in Python bedeutet:
Du kannst zeitaufwändige, I/O-lastige Aufgaben starten und in der Zwischenzeit andere Aufgaben erledigen, statt auf jede einzelne zu warten.
- Die Mechanismen dafür sind:
- `async def` (Coroutines),
- `await` (warten, ohne zu blockieren),
- ein **Event Loop** (z.B. in `asyncio`).
- Es löst besonders gut Probleme mit **vielen gleichzeitigen I/O-Operationen** (Netzwerk, Datenbanken), wie bei Webservern und Web-Scrapern.
- Herausforderungen:
- Umdenken gegenüber normalem, synchronem Code.
- Aufpassen, keine blockierenden Funktionen im Event Loop zu verwenden.
- Debugging und Testen sind etwas komplexer.
+499
View File
@@ -0,0 +1,499 @@
Im Folgenden bekommst du eine umfassende, aber einsteigerfreundliche Einführung in FastAPI.
---
## 1. Grundidee: Was ist FastAPI?
**FastAPI** ist ein modernes, schnelles Web-Framework für Python, mit dem du **Web-APIs** (Schnittstellen) bauen kannst.
Eine API ist eine „Schnittstelle“, über die andere Programme mit deinem Programm sprechen können – z. B. eine Web-App, ein Mobile-App-Backend oder interne Services in einem Unternehmen.
Kernpunkte von FastAPI:
- **Schwerpunkt:** Aufbau von **HTTP-APIs** (REST-APIs, JSON-basierte APIs).
- **Geschwindigkeit:** Sehr performant durch Nutzung von **asynchronem Python** (`async`/`await`), basierend auf **ASGI**.
- **Typisierung:** Starke Nutzung von **Python-Typannotationen** (z. B. `str`, `int`, eigene Klassen).
→ Daraus entstehen automatisch:
- Validierung von Daten,
- automatische Dokumentation (Swagger / OpenAPI),
- bessere IDE-Unterstützung (Autovervollständigung, Fehlererkennung).
- **Auto-Dokumentation:** FastAPI generiert automatisch eine **interaktive API-Dokumentation** im Browser.
Ein typisches „Hello World“ mit FastAPI sieht so aus:
```python
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello World"}
```
Starten kannst du das z. B. mit:
```bash
uvicorn main:app --reload
```
Dann ist die API z. B. unter `http://127.0.0.1:8000` erreichbar.
---
## 2. Abgrenzung: FastAPI vs. verwandte Begriffe und Frameworks
### 2.1 FastAPI vs. „API“ / REST / HTTP allgemein
- **HTTP**: Das zugrunde liegende Protokoll, über das Browser oder andere Dienste kommunizieren.
- **REST-API**: Eine Art, HTTP-APIs zu strukturieren (z. B. `GET /users`, `POST /orders`).
- **FastAPI**: Ein **Framework**, das dir hilft, solche HTTP/REST-APIs in Python zu bauen.
FastAPI „spricht“ also HTTP, baut REST-APIs, ist aber selbst das **Werkzeug**, kein Protokoll.
---
### 2.2 FastAPI vs. Flask
**Flask** ist ein sehr bekanntes, minimalistisches Python-Webframework.
**Ähnlichkeiten:**
- Beide erlauben es, mit wenig Code HTTP-Endpunkte zu definieren.
- Beide sind relativ leichtgewichtig und flexibel.
**Unterschiede:**
- **Asynchronität**:
- Flask: traditionell synchron (WSGI), Async ist erst neuerdings und eingeschränkt verfügbar.
- FastAPI: von Anfang an für **async** gebaut (ASGI).
- **Typen & Validierung**:
- Flask: Kein eingebautes System für automatische Validierung – du machst das selbst oder mit Erweiterungen.
- FastAPI: Nutzt **[[Pydantic]]**-Modelle und Typannotationen → automatische Validierung.
- **Dokumentation**:
- Flask: Kein automatisches API-Dokumentations-UI.
- FastAPI: Automatisch generierte OpenAPI/Swagger-UI unter `/docs` und `/redoc`.
Praxisbeispiel Vergleich:
**Flask:**
```python
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/items", methods=["POST"])
def create_item():
data = request.get_json()
name = data.get("name")
price = data.get("price")
if not isinstance(name, str) or not isinstance(price, (int, float)):
return jsonify({"error": "Invalid data"}), 400
return jsonify({"name": name, "price": price})
```
**FastAPI:**
```python
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
@app.post("/items")
def create_item(item: Item):
# item ist schon validiert
return item
```
FastAPI übernimmt hier die Validierung automatisch.
---
### 2.3 FastAPI vs. Django (und Django REST Framework)
**Django** ist ein „Full-Stack“-Framework:
- liefert Templates, ORM (Datenbankzugriff), Admin-Interface, Auth-System, Formulare etc.
- ideal für klassische Webanwendungen mit HTML-Seiten.
Für APIs nutzt man meist **Django REST Framework (DRF)** als Erweiterung.
**FastAPI** dagegen ist:
- stärker auf **APIs** fokussiert,
- nicht „alles aus einer Hand“, sondern:
- Web-Layer: `Starlette`,
- Datenvalidierung: `Pydantic`,
- Datenbank: du wählst selbst z. B. SQLAlchemy, Tortoise ORM etc.
Faustregel:
- Wenn du eine klassische Website mit HTML-Rendering brauchst → Django.
- Wenn du primär eine performant API bauen willst (z. B. für SPA, Microservices) → FastAPI ist sehr attraktiv.
---
### 2.4 FastAPI vs. Node.js / Express
**Node.js + Express** ist eine sehr verbreitete Lösung für APIs in JavaScript/TypeScript.
- **Sprache:** Node → JavaScript/TypeScript, FastAPI → Python.
- **Typen:** TypeScript kann Typen bieten, FastAPI nutzt Python-Typen + [[Pydantic]].
- **Ökosystem:** Node sehr stark im Web-/Frontend-nahen Bereich, Python stark bei Data Science, Machine Learning und Backend-Services.
FastAPI ist besonders interessant, wenn du sowieso Python nutzt (z. B. wegen ML/AI) und dafür eine passende Web-API brauchst.
---
## 3. Welche Probleme löst FastAPI?
### 3.1 Saubere, valide Eingabedaten
Problem ohne Framework-Unterstützung:
- Du bekommst z. B. einen JSON-Body und musst:
- alle Felder prüfen (Typ, Pflichtfelder, Wertebereiche),
- Fehler verständlich zurückgeben,
- alles manuell machen.
FastAPI + [[Pydantic]] lösen das:
```python
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class User(BaseModel):
name: str = Field(..., min_length=3)
age: int = Field(..., ge=0, le=120) # 0 <= age <= 120
@app.post("/users")
def create_user(user: User):
# Wenn name zu kurz oder age negativ ist, liefert FastAPI automatisch 422 mit Fehlerdetails
return {"message": "User created", "user": user}
```
Vorteil:
- Weniger Fehleranfälligkeit.
- Konsistente Fehlerantworten.
- Gute Developer-Erfahrung.
---
### 3.2 Automatische Dokumentation und Testbarkeit
FastAPI erzeugt automatisch eine OpenAPI-Spezifikation und UI:
- `http://localhost:8000/docs` → Swagger UI (interaktive Oberfläche, du kannst Requests direkt aus dem Browser abschicken).
- `http://localhost:8000/redoc` → ReDoc, alternative Dokumentationsansicht.
Das hilft:
- Dir selbst beim Testen.
- Frontend-Entwicklern oder anderen Teams, die deine API nutzen.
- Beim automatisierten Generieren von Client-SDKs (z. B. TypeScript-Client).
---
### 3.3 Performance und asynchrones I/O
Problem:
- In „klassischen“ synchronen Webframeworks blockiert jeder Request, der z. B. auf eine externe API oder langsame DB wartet.
- Bei vielen gleichzeitigen Anfragen leiden Durchsatz und Antwortzeit.
FastAPI setzt auf **ASGI** (Asynchronous Server Gateway Interface) und `async def`:
```python
from fastapi import FastAPI
import httpx # asynchroner HTTP-Client
app = FastAPI()
@app.get("/external")
async def call_external_api():
async with httpx.AsyncClient() as client:
response = await client.get("https://httpbin.org/get")
return response.json()
```
Vorteil:
- Viele I/O-lastige Requests können parallel abgewickelt werden.
- Besonders sinnvoll bei Microservices, die viel mit anderen Services kommunizieren.
---
### 3.4 Abhängigkeiten und Wiederverwendbarkeit (Dependency Injection)
FastAPI bietet ein eingebautes **Dependency Injection**-System.
Beim Entwickeln von APIs brauchst du häufig:
- Datenbankverbindungen,
- Authentifizierungslogik,
- Konfigurationsobjekte.
Ohne System würdest du das überall wiederholen oder global speichern.
Mit FastAPI:
```python
from fastapi import Depends, FastAPI
app = FastAPI()
def get_settings():
# z. B. Konfiguration laden
return {"app_name": "Meine App"}
@app.get("/info")
def read_info(settings = Depends(get_settings)):
return {"app_name": settings["app_name"]}
```
Das verbessert:
- Testbarkeit (du kannst Dependencies im Test austauschen),
- Struktur deines Codes (klarere Trennung von Zuständigkeiten).
---
## 4. Typische Einsatzszenarien (praxisnah)
### Beispiel 1: Einfaches CRUD für ein „Item“
```python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List
app = FastAPI()
class Item(BaseModel):
id: int
name: str
price: float
# „Fake-Datenbank“ im Speicher
items_db: List[Item] = []
@app.post("/items", response_model=Item)
def create_item(item: Item):
# einfache Prüfung: ID darf nicht doppelt sein
if any(existing.id == item.id for existing in items_db):
raise HTTPException(status_code=400, detail="Item ID already exists")
items_db.append(item)
return item
@app.get("/items", response_model=List[Item])
def list_items():
return items_db
@app.get("/items/{item_id}", response_model=Item)
def get_item(item_id: int):
for item in items_db:
if item.id == item_id:
return item
raise HTTPException(status_code=404, detail="Item not found")
```
Du bekommst:
- JSON-APIs für CRUD,
- automatische Dokumentation,
- automatische Validierung für `Item`.
---
### Beispiel 2: Path-Parameter, Query-Parameter, Body
```python
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
app = FastAPI()
class SearchFilters(BaseModel):
min_price: Optional[float] = None
max_price: Optional[float] = None
@app.get("/products/{category}")
def search_products(
category: str,
q: Optional[str] = None, # Query-Parameter ?q=Text
filters: SearchFilters = None # Body JSON
):
return {
"category": category,
"query": q,
"filters": filters
}
```
Beispiel-Request:
- `GET /products/books?q=python` mit JSON-Body:
```json
{
"min_price": 10,
"max_price": 50
}
```
FastAPI erkennt:
- `category` als Pfadparameter,
- `q` als Query-Parameter,
- `filters` als JSON-Body und validiert ihn.
---
### Beispiel 3: Einfache Authentifizierung per Token
```python
from fastapi import Depends, FastAPI, HTTPException, Header
app = FastAPI()
def get_current_user(x_token: str = Header(...)):
if x_token != "secrettoken123":
raise HTTPException(status_code=401, detail="Invalid or missing token")
return {"username": "alice"}
@app.get("/profile")
def read_profile(current_user = Depends(get_current_user)):
return {"message": f"Hello, {current_user['username']}"}
```
Hier:
- Der Endpunkt `/profile` verlangt einen HTTP-Header `X-Token`.
- FastAPI übernimmt das Zusammenspiel von Header → Dependency → Endpoint-Logik.
---
## 5. Herausforderungen und typische Stolpersteine
FastAPI nimmt dir viel ab, aber es gibt einige Themen, die für Einsteiger Hürden sein können:
### 5.1 Asynchrones Programmieren (`async` / `await`)
- Wenn du noch nie mit Async gearbeitet hast, ist es ungewohnt:
- Wann nutze ich `async def`?
- Wo brauche ich `await`?
- Was ist „Blocking I/O“?
- Du musst darauf achten, dass du **asynchrone Bibliotheken** verwendest, wenn du im Handler `async` einsetzt
(z. B. `httpx` statt `requests`, `asyncpg` statt `psycopg2`).
Wenn du erst mal einsteigst, kannst du auch erst **synchron** (ohne `async`) starten und später umstellen.
---
### 5.2 Typannotationen und Pydantic verstehen
FastAPI baut stark auf Typen auf:
- Für jemanden ohne Erfahrung mit Typannotationen in Python ist das anfangs ungewohnt.
- Du musst verstehen:
- Wie du eigene Modelle mit `BaseModel` definierst.
- Wie optionale Felder mit `Optional[...]` und Standardwerten funktionieren.
- Wie Validierung und Fehlernachrichten von [[Pydantic]] aussehen.
Aber:
Der Lerneffekt lohnt sich, weil du insgesamt saubereren, stabileren Code bekommst.
---
### 5.3 Datenbankintegration
FastAPI selbst bringt keinen [[ORM]] mit. Du musst wählen:
- z. B. **[[SQLAlchemy]]**, Tortoise-ORM, Prisma, Gino etc.
Dabei stellen sich Fragen wie:
- Wie verwalte ich Datenbank-Sessions pro Request?
- Nutze ich die sync- oder async-Variante meiner ORM?
- Wie realisiere ich Migrations (alembic, etc.)?
Es gibt viele Beispielprojekte, aber es ist ein zusätzlicher Schritt im Vergleich zu Django, wo ein [[ORM]] „eingebaut“ ist.
---
### 5.4 Deployment / Betrieb
Für Einsteiger ist der Weg von „läuft lokal“ zu „läuft im Internet“ oft herausfordernd:
- FastAPI-Anwendung läuft typischerweise mit:
- [[uvicorn]] oder [[hypercorn]] (ASGI-Server),
- oft hinter einem Reverse Proxy wie [[Nginx]].
- Themen:
- Logging konfigurieren,
- Umgebungsvariablen (Konfiguration),
- HTTPS/SSL (z. B. via [[Nginx]]/Let’s Encrypt),
- Skalierung (mehrere Worker, z. B. `gunicorn` + `uvicorn.workers.UvicornWorker`).
Für den Anfang kannst du auch auf Plattformen wie Render, Railway, fly.io, oder Docker + Cloud setzen.
---
### 5.5 Versionierung und Wartung großer Projekte
Bei größeren APIs:
- Wie strukturiere ich meinen Code?
- z. B. mit **Routern** (`APIRouter`) und Modulen.
- Wie versioniere ich die API? (`/v1/user`, `/v2/user`, …)
- Wie halte ich die Dokumentation aktuell?
- FastAPI hilft zwar, aber bei vielen Endpunkten braucht man Konventionen und ggf. zusätzliche Dokumentation.
Beispiel mit Router:
```python
from fastapi import FastAPI, APIRouter
app = FastAPI()
items_router = APIRouter(prefix="/items", tags=["items"])
@items_router.get("/")
def list_items():
return [{"id": 1, "name": "Item 1"}]
app.include_router(items_router)
```
So kannst du größere Projekte modular strukturieren.
---
## 6. Zusammenfassung
- **FastAPI** ist ein modernes Framework für **Web-APIs in Python**, fokussiert auf:
- hohe **Performance** (async),
- **Typen** + automatische Validierung ([[Pydantic]]),
- automatische **OpenAPI-/Swagger-Dokumentation**,
- gute Developer Experience.
- Es unterscheidet sich von:
- **Flask**: moderner, stärker typisiert, async-first, integrierte Validierung & Doku.
- **Django**: kein Full-Stack-Framework, sondern eher API-fokussiert; du kombinierst es mit eigenen Tools für DB, Templates etc.
- Node/Express: andere Sprache, andere Ökosysteme; FastAPI besonders stark, wenn du ohnehin Python nutzt.
- Es löst typische Probleme beim API-Bau:
- Validierung von Eingaben,
- Dokumentation & Testbarkeit,
- Performance bei vielen gleichzeitigen Anfragen,
- saubere Struktur durch Dependency Injection.
- Herausforderungen:
- Einstieg in asynchrones Programmieren,
- Verständnis von Typannotationen & [[Pydantic]],
- separate Auswahl & Integration einer Datenbanklösung,
- Deployment & Betrieb.
+470
View File
@@ -0,0 +1,470 @@
Pydantic ist ein zentrales Werkzeug im heutigen Python-Ökosystem, vor allem im Umfeld von APIs (z.B. [[FastAPI]]), Konfiguration und Datenvalidierung.
Im Folgenden bekommst du eine systematische Einführung mit praxisnahen Beispielen.
**Hinweis:** Die Beispiele orientieren sich an **Pydantic v2** (aktuelle Hauptversion). In v1 ist die Syntax ähnlich, aber es gibt einige Unterschiede (z.B. `@validator` vs. `@field_validator`).
---
## 1. Grundsätzliche Definition: Was ist Pydantic?
**Kurz:**
Pydantic ist eine Bibliothek für **Datenmodelle mit Validierung und Parsing** auf Basis von **Python-Typannotationen**.
Du beschreibst deine Datenstruktur wie bei einer Klasse mit Typen:
- Pydantic prüft zur Laufzeit, ob eingehende Daten diese Struktur erfüllen.
- Es konvertiert (parst) Werte soweit wie möglich in die gewünschten Typen.
- Es gibt strukturierte Fehlermeldungen aus, wenn etwas nicht passt.
- Es kann aus deinen Modellen u.a. **JSON-Schemas** generieren.
Beispiel – ein einfaches Datenmodell:
```python
from pydantic import BaseModel, ValidationError
from typing import List
class User(BaseModel):
id: int
name: str
tags: List[str] = []
# Daten aus einer externen Quelle (z.B. JSON)
payload = {
"id": "123", # wird zu int konvertiert
"name": "Alice",
"tags": ["admin", "beta"]
}
user = User(**payload)
print(user)
print(user.id, type(user.id))
# Fehlvalidierung
try:
User(id="abc", name=123)
except ValidationError as e:
print(e.errors())
```
Wichtige Punkte:
- `id` ist als `int` deklariert, ein String `"123"` wird automatisch konvertiert.
- Wenn Konvertierung scheitert (z.B. `"abc"` → `int`), erzeugt Pydantic eine **ValidationError** mit detailierten Fehlerinfos.
---
## 2. Grundkonzepte von Pydantic (v2)
### 2.1 BaseModel und Felder
Alle Modelle erben typically von `BaseModel`:
```python
from pydantic import BaseModel, Field
from typing import Optional
class Product(BaseModel):
id: int
name: str = Field(..., min_length=3, description="Produktname")
price: float = Field(ge=0)
description: Optional[str] = None
```
- `Field(...)` bedeutet „Pflichtfeld“ mit zusätzlichen Metadaten/Constraints.
- `ge=0` = „greater or equal 0“.
- `Optional[str] = None` = optionales Feld, default `None`.
### 2.2 Validierung & Parsing
Pydantic führt **Validierung und Parsing beim Erstellen** des Modells durch.
Man kann explizit „parsen“:
```python
from pydantic import TypeAdapter
from typing import List
# Einzelnes Modell: meistens direkt Model(**data)
product = Product(id="1", name="TV", price="999.90")
# Sammlung von Modellen validieren:
ta = TypeAdapter(List[Product])
data = [
{"id": 1, "name": "TV", "price": 999.90},
{"id": "2", "name": "Laptop", "price": "1299.50"},
]
products = ta.validate_python(data)
print(products)
```
`TypeAdapter` in v2 ersetzt viele frühere `parse_obj_as`-Usecases.
### 2.3 Serialisierung
Pydantic-Modelle lassen sich leicht in z.B. JSON-kompatible Strukturen umwandeln:
```python
product = Product(id=1, name="TV", price=999.90)
print(product.model_dump()) # dict
print(product.model_dump_json()) # JSON-String
```
Man kann steuern:
- welche Felder inkludiert/exkludiert werden,
- wie verschachtelte Modelle serialisiert werden,
- ob Alias-Namen verwendet werden sollen etc.
---
## 3. Abgrenzung zu verwandten Konzepten / Bibliotheken
### 3.1 Pydantic vs. `dataclasses`
Python `dataclasses`:
```python
from dataclasses import dataclass
@dataclass
class UserDC:
id: int
name: str
```
- `dataclasses` stellen nur **strukturelle Container** bereit.
- Keine automatische Validierung oder Typkonvertierung.
- Typannotationen sind rein informativ (für IDE, mypy), nicht enforced.
Pydantic:
```python
class UserModel(BaseModel):
id: int
name: str
```
- Führt **Validierung & Parsing** beim Erstellen durch.
- Gibt strukturierte Fehler aus.
- Generiert optional JSON-Schemas.
- Basiert auch auf Typannotationen, aber **wertet sie zur Laufzeit aus**.
Kurz:
- `dataclasses`: leichtgewichtige Container.
- Pydantic: Container + Validierung + Parsing + Schema.
### 3.2 Pydantic vs. Marshmallow / Cerberus u.ä.
- **Marshmallow** ist ebenfalls eine Validierungs-/Serialisierungsbibliothek.
- Du definierst Schemas explizit über Felder (z.B. `fields.Int()`) statt über Typannotationen.
- Skill: starke Serialisierung/Deserialisierung, aber andere API.
- **Pydantic**:
- Nutzt standardmäßige Python-Typannotationen (nativer für moderne Python-Code).
- Sehr eng mit Typing-Ökosystem (mypy, IDEs).
- Performance-fokussiert, in v2 mit `pydantic-core` in Rust.
### 3.3 Pydantic vs. Typing-Features (`TypedDict`, `Protocol`, …)
- `TypedDict` definiert nur statische Typinformationen für Dictionaries.
- Pydantic-Modelle sind **richtige Klassen** mit Methoden, Validierung und Verhalten.
### 3.4 Pydantic vs. ORMs (z.B. Django Models, SQLAlchemy Models)
- [[ORM]]-Modelle repräsentieren **Datenbanktabellen** und kümmern sich um **Persistenz** (CRUD, Queries).
- Pydantic-Modelle sind **reine Daten- und Validierungsmodelle**, ohne DB-Anbindung.
In der Praxis:
- Du kannst Pydantic-Modelle nutzen, um **Requests/Responses** zu validieren und zu dokumentieren.
- ORMs nutzen, um die Daten in der Datenbank zu speichern.
[[FastAPI]] macht genau das:
- Pydantic-Modelle für Request/Response,
- SQLAlchemy/SQLModel/etc. für DB.
---
## 4. Welche Probleme löst Pydantic?
### 4.1 Validierung externer Daten (APIs, Formulare, Message Queues)
Externe Daten sind oft:
- unvollständig,
- im falschen Typ,
- fehlerhaft strukturiert.
Pydantic sorgt für:
- Zentral definierte Datenstruktur.
- Automatische Validierung bei jedem Eingang.
- Konvertierung (z.B. `"123"` → `int`, `"2024-01-01"` → `datetime`).
Beispiel: Request-Daten einer (pseudo) API:
```python
from pydantic import BaseModel, HttpUrl
from typing import List
class Article(BaseModel):
title: str
url: HttpUrl
tags: List[str] = []
payload = {
"title": "Pydantic Einführung",
"url": "https://example.com/pydantic",
"tags": ["python", "validation"]
}
article = Article(**payload)
print(article)
```
Wenn `url` kein gültiger URL-String ist, kommt eine strukturierte Fehlermeldung.
### 4.2 Konfiguration und Umgebungsvariablen
Pydantic kann Konfiguration aus:
- Umgebungsvariablen,
- `.env`-Dateien,
- kwargs,
- etc.
laden und validieren.
In v2 nutzt man `pydantic-settings`:
```python
from pydantic_settings import BaseSettings
class AppSettings(BaseSettings):
debug: bool = False
database_url: str
port: int = 8000
model_config = {
"env_file": ".env",
"env_prefix": "APP_",
}
settings = AppSettings()
print(settings.database_url, settings.debug)
```
- `APP_DATABASE_URL` in der Umgebung oder `.env` wird gelesen.
- Falsche Typen werden validiert/konvertiert (z.B. `"true"` → `bool`).
- Fehlende Pflichtwerte (z.B. `database_url`) führen zu Fehlern.
### 4.3 Saubere Domain-Modelle und Business-Logik
Du kannst Pydantic-Modelle verwenden, um deine Domain-Objekte zu modellieren, inklusive:
- Validierung von Invarianten (z.B. Preis > 0, Datum in der Zukunft/ Vergangenheit),
- Standardwerte,
- abgeleitete Felder.
```python
from pydantic import BaseModel, field_validator
from datetime import datetime
class Event(BaseModel):
name: str
start: datetime
end: datetime
@field_validator("end")
def end_must_be_after_start(cls, v, info):
start = info.data.get("start")
if start and v <= start:
raise ValueError("end must be after start")
return v
```
---
## 5. Herausforderungen & Stolpersteine
### 5.1 Performance und Overhead
- Pydantic führt bei **jedem Instanziieren** eines Modells Validierung/Parsing durch.
- Bei sehr großen Datenmengen oder sehr häufigen Instanziierungen kann das Performance kosten.
- *Lösung*: gezielt einsetzen, ggf. `model_validate` mit `from_attributes=True` o.Ä., Caching, oder an bestimmten Stellen auf „raw“ Datenstrukturen ausweichen.
### 5.2 Lax vs. Strict Typen
Standardmäßig ist Pydantic recht **„freundlich“**:
- `"123"` → `int(123)`
- `"true"` → `bool(True)` (bei Settings)
- `"1.23"` → `float(1.23)`
Das ist praktisch, kann aber auch unerwartete Effekte haben.
Du kannst **strict**-Typen verwenden oder striktere Konfiguration:
```python
from pydantic import BaseModel, StrictInt
class Model(BaseModel):
value: StrictInt
# Model(value="1") -> ValidationError (keine Autokonvertierung)
```
Oder über `model_config`:
```python
class Model(BaseModel):
value: int
model_config = {
"strict": True,
}
```
### 5.3 Umgang mit Optional, Defaults, Required
Typische Stolperfallen:
```python
from typing import Optional
from pydantic import BaseModel, Field
class Example(BaseModel):
a: int # Pflichtfeld
b: Optional[int] # „darf None sein“, aber kein Default → ebenfalls Pflichtfeld
c: int = 0 # optional, default = 0
d: Optional[int] = None # optional, default = None
e: int = Field(..., description="explizit required") # Pflichtfeld
```
- `Optional[int]` heißt nur „`int` oder `None`“, nicht automatisch optional im Sinne von „nicht im Input vorhanden“.
- „Required“ bedeutet: Feld muss im Input vorhanden sein, außer es gibt einen Default.
### 5.4 Migration v1 → v2
Wenn du Codebeispiele im Netz findest, sind viele noch Pydantic v1:
- `@validator` wurde größtenteils zu `@field_validator`.
- `parse_obj_as` → `TypeAdapter`.
- `Config`-Inner-Class → `model_config` oder `ConfigDict`.
Beim Einstieg: gleich v2-Doku lesen und wählen.
### 5.5 Komplexe verschachtelte Strukturen
Pydantic kann sehr komplexe Strukturen validieren (verschachtelte Modelle, Union-Typen, generische Modelle).
Herausforderung ist eher das **Verständnis** der Typen und Validierungsreihenfolge.
---
## 6. Praxisnahe Beispiele
### 6.1 Verschachtelte Modelle
```python
from pydantic import BaseModel
from typing import List
class Address(BaseModel):
street: str
city: str
zip_code: str
class Customer(BaseModel):
id: int
name: str
addresses: List[Address]
data = {
"id": "1",
"name": "Bob",
"addresses": [
{"street": "Main St 1", "city": "Berlin", "zip_code": "10115"},
{"street": "Side St 2", "city": "Hamburg", "zip_code": "20095"},
]
}
customer = Customer(**data)
print(customer)
```
Fehler in einer Adresse werden detailliert auf der jeweiligen „Pfad“-Ebene ausgegeben.
### 6.2 Feld-Constraints & Metadaten
```python
from pydantic import BaseModel, Field
from typing import Literal
class Order(BaseModel):
id: int
status: Literal["open", "paid", "shipped"]
quantity: int = Field(gt=0, description="Muss > 0 sein")
customer_email: str = Field(pattern=r"[^@]+@[^@]+\.[^@]+")
order = Order(
id=1,
status="open",
quantity=5,
customer_email="test@example.com"
)
```
- `Literal` beschränkt mögliche Werte (Enum-artig).
- `pattern` (Regex) validiert z.B. einfache E-Mail-Formate.
### 6.3 Custom Validierung mit `field_validator` und `model_validator`
```python
from pydantic import BaseModel, field_validator, model_validator
class User(BaseModel):
username: str
password: str
password_repeat: str
@field_validator("username")
def username_not_empty(cls, v):
if not v.strip():
raise ValueError("username must not be empty")
return v
@model_validator(mode="after")
def passwords_match(self):
if self.password != self.password_repeat:
raise ValueError("passwords do not match")
return self
```
- `field_validator` prüft einzelne Felder.
- `model_validator` (v2) hat Zugriff auf das ganze Modell (z.B. um zwei Felder zu vergleichen).
### 6.4 JSON-Schema / OpenAPI-Integration
Pydantic kann JSON-Schemas erzeugen, die u.a. von [[FastAPI]] genutzt werden, um automatisch Doku (OpenAPI/Swagger) zu generieren:
```python
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
print(Item.model_json_schema())
```
Das ausgegebene Schema beschreibt die Struktur, Typen und Constraints – ideal für API-Dokumentation.
---
## 7. Zusammenfassung
- **Definition:** Pydantic ist eine Python-Bibliothek für **Datenmodelle mit Validierung, Parsing und Serialisierung** auf Basis von Typannotationen.
- **Abgrenzung:**
- Mehr als `dataclasses` (mit Validierung & Parsing).
- Nutzt Python-Typing natürlicher als Marshmallow & Co.
- Kein ORM, sondern ergänzt diese (oft für API-Schicht).
- **Probleme, die gelöst werden:**
- Validierung externer Daten (APIs, Config, User-Input).
- Typ-sichere Domain-Modelle.
- Konfiguration aus Umgebungsvariablen/Dateien inkl. Typenprüfung.
- Automatische Generierung von JSON-Schemas (z.B. für APIs).
- **Herausforderungen:**
- Performance bei massiver Nutzung.
- Verständnis von strict vs. lax Typen.
- Stolperfallen bei Optional/Defaults.
- Versionsunterschiede (v1 vs. v2).
+1170
View File
File diff suppressed because it is too large Load Diff
+368
View File
@@ -0,0 +1,368 @@
## 1. Grundidee von `argparse`
`argparse` ist das Standardmodul in Python, um Kommandozeilen-Argumente zu definieren, zu parsen und automatisch Hilfe-/Usage-Texte zu erzeugen.
Minimalbeispiel:
```python
import argparse
parser = argparse.ArgumentParser(description="Ein kleines Beispiel-CLI")
parser.add_argument("datei", help="Pfad zur Eingabedatei")
args = parser.parse_args()
print(args.datei)
```
Aufruf:
```bash
python script.py meine_datei.txt
```
---
## 2. Argumente definieren: `ArgumentParser.add_argument`
### 2.1 Positionsargumente
- Werden ohne führende `-` oder `--` angegeben.
- Reihenfolge ist relevant.
```python
parser.add_argument("quelle", help="Quellpfad")
parser.add_argument("ziel", help="Zielpfad")
```
Aufruf:
```bash
python script.py input.txt output.txt
```
Use-Case:
- Pflichtwerte, die immer gebraucht werden (z.B. Eingabe- und Ausgabedatei).
---
### 2.2 Optionale Argumente (Flags / Optionen)
- Beginnen mit `-` bzw. `--`.
- Reihenfolge ist egal.
- Können Standardwerte haben.
```python
parser.add_argument(
"-v", "--verbose",
action="store_true",
help="Ausführliche Ausgabe aktivieren"
)
parser.add_argument(
"-n", "--anzahl",
type=int,
default=10,
help="Anzahl der Elemente (Standard: 10)"
)
```
Aufruf:
```bash
python script.py input.txt --verbose --anzahl 5
# oder kurz:
python script.py input.txt -v -n 5
```
Use-Case:
- Konfiguration, optionales Verhalten, Debug/Verbose-Flags, Parameter mit Default.
---
## 3. Wichtige Parameter von `add_argument`
### 3.1 `name` / `flags`
- Beispiel:
- Positional: `"datei"`
- Optional: `"-v", "--verbose"`
```python
parser.add_argument("datei")
parser.add_argument("-v", "--verbose")
```
---
### 3.2 `type`
- Convertiert Eingabe in Typ.
- Validiert automatisch (bei falschem Typ Fehler + Hilfe).
```python
parser.add_argument("--port", type=int, default=8080)
parser.add_argument("--faktor", type=float)
```
Use-Case:
- Numerische Werte, Pfade, eigene Typen (z.B. `Path` aus `pathlib`).
---
### 3.3 `default`
- Standardwert, wenn Argument nicht übergeben wird.
```python
parser.add_argument("--log-level", default="INFO")
```
Use-Case:
- Sinnvolle Defaults, um CLI kompakt zu halten.
---
### 3.4 `required`
- Macht optionale Argumente zwingend erforderlich.
```python
parser.add_argument("--config", required=True)
```
Use-Case:
- Flags/Optionen, die zwingend gesetzt werden müssen (z.B. API-Key, Konfigdatei).
---
### 3.5 `help`
- Beschreibung für die `--help`-Ausgabe.
```python
parser.add_argument("--mode", help="Betriebsmodus: fast oder safe")
```
Use-Case:
- Dokumentation der Optionen (sehr wichtig für Benutzerfreundlichkeit).
---
### 3.6 `choices`
- Schränkt erlaubte Werte ein.
```python
parser.add_argument(
"--mode",
choices=["fast", "safe"],
default="safe",
help="fast = schneller, safe = sicherer"
)
```
Use-Case:
- Enum-ähnliche Optionen (z.B. `debug/info/warn/error`, `json/text`).
---
### 3.7 `action`
Steuert, was passiert, wenn das Argument gesetzt wird.
Häufige Actions:
1. `store` (Standard)
Speichert den Wert (z.B. `--port 8000` → `args.port = 8000`).
2. `store_true` / `store_false`
Boolean-Flag, das `True`/`False` setzt.
```python
parser.add_argument("-v", "--verbose", action="store_true")
```
3. `append`
Fügt mehrere Werte in eine Liste ein.
```python
parser.add_argument(
"-t", "--tag",
action="append",
help="Kann mehrfach verwendet werden"
)
# Aufruf: --tag a --tag b -> args.tag = ["a", "b"]
```
4. `count`
Zählt, wie oft ein Flag verwendet wurde.
```python
parser.add_argument(
"-v", "--verbose",
action="count",
default=0,
help="Mehrfach verwenden für mehr Details"
)
# -v -> 1, -vv -> 2 ...
```
Use-Case:
- Flags (bool), Mehrfachangaben (Listen), Verbosity-Level etc.
---
### 3.8 `nargs`
Gibt an, wie viele Werte zu einem Argument gehören.
Typische Varianten:
- `nargs=1` → eine Liste mit einem Element
- `nargs=2` → genau 2 Werte
- `nargs="+"` → mindestens ein Wert
- `nargs="*"` → beliebig viele (auch 0)
```python
parser.add_argument("dateien", nargs="+", help="Eine oder mehrere Dateien")
parser.add_argument("--koordinaten", nargs=2, type=float, help="x y")
```
Use-Case:
- Mehrere Dateien, Koordinaten, Listen von Werten.
---
### 3.9 `metavar`
- Steuert, wie das Argument im Help-Text angezeigt wird.
```python
parser.add_argument(
"--output",
metavar="DATEI",
help="Ausgabedatei"
)
```
Use-Case:
- Schöner formatierte Hilfe (statt generischer Namen).
---
### 3.10 `dest`
- Name des Attributes in `args`.
```python
parser.add_argument("-o", "--output", dest="ausgabedatei")
# args.ausgabedatei
```
Use-Case:
- Lesbare/konfliktfreie Python-Bezeichner, wenn CLI-Namen nicht ideal sind.
---
## 4. Subkommandos: `subparsers`
Für CLI-Tools mit mehreren Befehlen (ähnlich `git commit`, `git status`).
```python
import argparse
parser = argparse.ArgumentParser(prog="tool")
subparsers = parser.add_subparsers(dest="command", required=True)
# Subkommando: "run"
run_parser = subparsers.add_parser("run", help="Job ausführen")
run_parser.add_argument("job_id", type=int)
# Subkommando: "list"
list_parser = subparsers.add_parser("list", help="Jobs auflisten")
list_parser.add_argument("--status", choices=["open", "done"])
args = parser.parse_args()
if args.command == "run":
print(f"Starte Job {args.job_id}")
elif args.command == "list":
print(f"Liste Jobs mit Status {args.status}")
```
Aufrufe:
```bash
tool run 42
tool list --status open
```
Use-Case:
- Umfangreiche Tools mit verschiedenen Befehlen (z.B. Admin-Tools, Deployment-CLI).
---
## 5. Automatische Hilfe und Usage
`argparse` erzeugt automatisch `-h` / `--help`:
```bash
python script.py --help
```
Du bekommst:
- Beschreibung (`description`)
- Liste aller Argumente
- Default-Werte (wenn konfiguriert)
- Subkommandos (falls vorhanden)
Beispiel:
```python
parser = argparse.ArgumentParser(
description="Konvertiert Dateien in andere Formate."
)
```
---
## 6. Minimaler „Best-Practice“-Skeleton
```python
import argparse
def parse_args():
parser = argparse.ArgumentParser(
description="Beispiel-Tool für argparse"
)
# Positionsargumente
parser.add_argument("eingabe", help="Eingabedatei")
# Optionale Argumente
parser.add_argument(
"-o", "--output",
help="Ausgabedatei (Standard: stdout)"
)
parser.add_argument(
"-v", "--verbose",
action="store_true",
help="Ausführliche Ausgabe"
)
parser.add_argument(
"--mode",
choices=["fast", "safe"],
default="safe",
help="Verarbeitungsmodus (Standard: safe)"
)
return parser.parse_args()
def main():
args = parse_args()
if args.verbose:
print(f"Starte in Modus {args.mode} mit Eingabe {args.eingabe}")
# weitere Logik…
if __name__ == "__main__":
main()
```
---
+525
View File
@@ -0,0 +1,525 @@
Hier eine umfassende, aber für Einsteiger verständliche Einführung in **black**, den „opinionated“ Python-Code-Formatter.
---
## 1. Grundidee von *black*
**Was ist black?**
- *black* ist ein **automatisches Formatierungs-Tool** für Python.
- Es ändert **nur die Formatierung**, nicht die Logik deines Codes.
- Es ist **„opinionated“**: Es gibt nur sehr wenige Einstellungen – black entscheidet den Stil für dich.
**Zentrale Idee:**
> „*You are not your code style.*“
> Statt darüber zu diskutieren, ob ein Leerzeichen hier oder dort besser ist, überlässt du das black.
>
> Ziel:
> - einheitlicher Stil
> - weniger Diskussionen in Code Reviews
> - Fokus auf inhaltliche Fehler, nicht auf Formatierung
---
## 2. Was macht black konkret?
Black nimmt deinen Python-Code, parst ihn und schreibt ihn nach festen Regeln neu. Beispiele:
### 2.1 Installation
```bash
pip install black
```
### 2.2 Einfache Nutzung
Eine einzelne Datei formatieren:
```bash
black main.py
```
Ein ganzes Projekt:
```bash
black .
```
Nur anzeigen, was geändert würde (ohne zu schreiben):
```bash
black --diff --check .
```
---
## 3. Praxisnahe Formatierungsbeispiele
### 3.1 Zeilenumbrüche und Einrückung
**Vorher:**
```python
def very_long_function_name(arg1,arg2,arg3,arg4,arg5,arg6,arg7=False,arg8=None):
return (arg1+arg2+arg3+arg4+arg5+arg6)
```
**Nachher (black):**
```python
def very_long_function_name(
arg1,
arg2,
arg3,
arg4,
arg5,
arg6,
arg7=False,
arg8=None,
):
return arg1 + arg2 + arg3 + arg4 + arg5 + arg6
```
Was passiert?
- Argumente werden sauber untereinander geschrieben.
- Operatoren (+) werden mit Leerzeichen versehen.
- Abschluss-Komma nach dem letzten Argument (hilft bei späteren Änderungen).
---
### 3.2 Strings und Anführungszeichen
Black bevorzugt fast immer **doppelte Anführungszeichen**.
**Vorher:**
```python
name = 'Alice'
message = 'Hello, ' + name + '!'
```
**Nachher:**
```python
name = "Alice"
message = "Hello, " + name + "!"
```
Ausnahmen:
- Wenn ein String doppelte Anführungszeichen enthält, kann black einzelne beibehalten, um weniger zu escapen.
---
### 3.3 Leerzeichen und Klammern
**Vorher:**
```python
result=(1+2)*3
if x==42:
print( 'Answer',x )
```
**Nachher:**
```python
result = (1 + 2) * 3
if x == 42:
print("Answer", x)
```
Black:
- fügt Leerzeichen um Operatoren hinzu (`1 + 2`, `x == 42`),
- entfernt unnötige Leerzeichen (`print( 'Answer',x )` → `print("Answer", x)`).
---
### 3.4 Collections (Listen, Dicts, Sets)
**Vorher:**
```python
config = {"host":"localhost","port":5432,"debug":True}
```
**Nachher:**
```python
config = {
"host": "localhost",
"port": 5432,
"debug": True,
}
```
Vorteile:
- Bessere Lesbarkeit
- Leicht, neue Einträge hinzuzufügen (wegen Abschluss-Komma).
---
### 3.5 Lange Ausdrücke
**Vorher:**
```python
query = session.query(User).filter(User.is_active==True, User.signup_date>=start_date, User.signup_date<=end_date).order_by(User.signup_date.desc())
```
**Nachher:**
```python
query = (
session.query(User)
.filter(
User.is_active == True,
User.signup_date >= start_date,
User.signup_date <= end_date,
)
.order_by(User.signup_date.desc())
)
```
Black bricht lange Zeilen so um, dass:
- sie unter der vorgegebenen Maximalbreite bleiben (standard: 88 Zeichen),
- die Struktur des Codes klarer sichtbar wird.
---
## 4. Abgrenzung zu verwandten Tools
### 4.1 Formatter vs. Linter vs. Typprüfer
- **Formatter** (Formatierer):
Passen das **Aussehen** deines Codes an – z. B. black, autopep8, yapf.
- **Linter**:
Finden mögliche **Fehler, Stilprobleme oder unschöne Konstrukte** – z. B. flake8, pylint, ruff.
- **Typprüfer**:
Prüfen, ob Typen konsistent sind (z. B. mit `typing`) – z. B. mypy, pyright.
Black ist **nur** ein Formatter.
---
### 4.2 Black vs. autopep8
**autopep8**:
- Ziel: Code so anpassen, dass er PEP8-konform ist.
- Orientierung direkt an den PEP8-Regeln.
- Viele Optionen (z. B. bestimmte Checks an- oder abschalten).
**black**:
- Ziel: **konsequenter, einheitlicher Stil** – nicht nur PEP8, sondern zusätzliche strenge Regeln.
- Sehr wenige Konfigurationsmöglichkeiten (bewusst!).
- Output ist oft deutlich „strenger” und einheitlicher als autopep8.
---
### 4.3 Black vs. yapf
**yapf**:
- Google-Tool zur Formatierung von Python.
- Sehr konfigurierbar: du kannst deinen Style stark beeinflussen (ähnlich wie bei C++/clang-format).
- Mehr Freiheit, aber dadurch auch mehr Diskussionen möglich.
**black**:
- „Meine Regeln oder gar nicht“.
- Ziel: Diskussionen vermeiden, daher kaum Konfigurationsoptionen.
- Sehr stabiler, vorhersehbarer Output.
---
### 4.4 Black vs. isort
**isort** ist ein Tool, um **Imports** zu sortieren und zu gruppieren.
- Sortiert `import`-Zeilen alphabetisch und nach Gruppen:
- Standardbibliothek
- Third-Party
- Projektinterne Module
Black:
- Formatiert auch Imports (Zeilenumbrüche, Leerzeichen),
- sortiert sie aber **nicht** nach Paketnamen.
Typischerweise nutzt man:
```bash
isort .
black .
```
Oder beides zusammen über Tools wie `ruff` oder `pre-commit`.
---
## 5. Welche Probleme löst black?
### 5.1 Konsistenter Stil in Teams
Ohne Tool:
- Jeder schreibt „sein“ Python.
- Unterschiedliche Leerzeichen, Umbrüche, String-Stile, etc.
- Code wirkt „bunt“ und uneinheitlich.
Mit black:
- Jedes Commit, jede Datei, jede Funktion hat denselben Stil.
- Neue Teammitglieder lernen schneller, was „üblich“ ist – es ist einfach: das, was black macht.
---
### 5.2 Weniger Diskussionen in Code Reviews
Vor black:
- Kommentare wie: „Bitte hier ein Leerzeichen einfügen.“
- „Kannst du die Argumente untereinander schreiben?“
- „Wir verwenden eigentlich doppelte Anführungszeichen.“
Mit black:
- Reviewer sagen: „Bitte einmal black drüber laufen lassen.“
- Fokus liegt auf:
- Ist der Algorithmus korrekt?
- Sind die Funktionen gut benannt?
- Sind Tests vorhanden?
---
### 5.3 Bessere Lesbarkeit & Wartbarkeit
- Lange Zeilen werden sinnvoll umgebrochen.
- Verschachtelte Ausdrücke werden strukturiert.
- Datensammlungen (Listen, Dicts) werden mehrzeilig und übersichtlich dargestellt.
Beispiel: Eine unübersichtliche Dict-Liste wird automatisch gut lesbar formatiert.
**Vorher:**
```python
users=[{"id":1,"name":"Alice","active":True},{"id":2,"name":"Bob","active":False}]
```
**Nachher:**
```python
users = [
{"id": 1, "name": "Alice", "active": True},
{"id": 2, "name": "Bob", "active": False},
]
```
---
### 5.4 Weniger „Rauschen“ in Git-Diffs
Manuell Änderungen + Formatierung:
- Du änderst eine Zeile, formatierst etwas,
- der Diff zeigt viele Änderungen, obwohl nur wenig Logik geändert wurde.
Mit black:
- Wenn alle Dateien bereits formatiert sind, entstehen bei späteren Änderungen klarere Diffs:
- Format ist überall gleich,
- nur die wirklich geänderte Logik fällt auf.
---
## 6. Herausforderungen und typische Stolpersteine
### 6.1 Einstieg in ein bestehendes Projekt
Problem:
- Du führst black in einem **alten, großen Projekt** ein.
- Beim ersten Durchlauf ändert black Hunderte/tausende Dateien.
- Git-Diff ist riesig.
Lösungen / Best Practices:
- Einmaliger „Formatting-Commit“ (nur Style):
- In einem eigenen Commit alle Dateien mit black formatieren.
- Danach neue Commits nur mit funktionalen Anpassungen.
- Oder schrittweise:
- Nur neue/aktuell bearbeitete Module mit black formatieren.
- z. B. mit `pre-commit`-Hook nur geänderte Dateien behandeln.
---
### 6.2 „Mir gefällt der Stil nicht!“
Black ist sehr strikt:
- Du kannst nicht „mal eben“ sagen:
- „Ich möchte lieber 120 statt 88 Zeichen pro Zeile“ (ok, das **geht** als Option)
- Aber: Du kannst nicht festlegen, wie exakt bestimmte Konstrukte formatiert werden.
Beispiel: Viele stören sich zunächst an:
- doppelten Anführungszeichen,
- „ungewöhnlichen“ Zeilenumbrüchen.
Wichtig:
- black ist ein **Team-Tool**.
- Man einigt sich darauf, den Stil zu akzeptieren, um Diskussionen zu vermeiden.
---
### 6.3 Integration mit anderen Tools (z. B. isort, flake8, ruff)
Typische Stolperfallen:
- `isort` und `black` können sich widersprechen, wenn sie unterschiedliche Maximalzeilenlängen nutzen.
- Linter können sich über Formatierung beschweren, wenn sie anders konfiguriert sind als black.
Best Practice:
- Einheitliche Konfiguration, z. B. in `pyproject.toml`:
```toml
[tool.black]
line-length = 88
[tool.isort]
profile = "black"
line_length = 88
```
So passen Formatierung und Importsortierung zusammen.
---
### 6.4 Änderungen zwischen Black-Versionen
Black entwickelt sich weiter.
- In seltenen Fällen ändert eine neue Version den Stil leicht.
- Dann kann ein erneuter Durchlauf große Diffs erzeugen.
Abhilfe:
- Black-Version in `pyproject.toml` oder `requirements.txt` fest pinnen:
```text
black==24.4.2
```
- Gelegentlich bewusst aktualisieren und einmaliger Reformatting-Commit.
---
### 6.5 Performance bei sehr großen Projekten
Für normale Projekte ist black schnell genug.
Bei **sehr großen Repositories** kann einmaliges Formatieren aber dauern.
Lösungen:
- Nur geänderte Dateien formatieren (z. B. über `pre-commit`).
- In CI nur `black --check .` laufen lassen (prüft, ob alles formatiert ist, ohne neu zu schreiben).
---
## 7. Black in der Praxis: Workflows
### 7.1 Integration in den Editor/IDE
Die meisten Editoren können black beim Speichern ausführen:
- **VS Code**:
- Erweiterung „Python“ installieren.
- In `settings.json`:
```json
{
"python.formatting.provider": "black",
"editor.formatOnSave": true
}
```
- **PyCharm**:
- Externes Tool konfigurieren oder über Plugins.
Vorteil:
- Du denkst nicht mehr über Formatierung nach: Speichern = fertig formatiert.
---
### 7.2 Verwendung mit `pre-commit`
`pre-commit` ist ein Tool, das Skripte ausführt, bevor ein Git-Commit akzeptiert wird.
Installieren:
```bash
pip install pre-commit
```
In `.pre-commit-config.yaml`:
```yaml
repos:
- repo: https://github.com/psf/black
rev: 24.4.2 # Version anpassen
hooks:
- id: black
```
Aktivieren:
```bash
pre-commit install
```
Jetzt wird bei jedem `git commit` black auf die geänderten Dateien angewandt.
Das stellt sicher, dass alles im Repo immer formatiert ist.
---
### 7.3 Black in der CI-Pipeline
In CI-Systemen (GitHub Actions, GitLab CI, etc.) kannst du prüfen, ob der Code schon formatiert ist.
Beispiel (CLI):
```bash
black --check .
```
- Exit-Code 0: alles ok.
- Exit-Code ≠ 0: irgendetwas müsste formatiert werden.
Das verhindert, dass unformatierter Code in `main` oder `master` landet.
---
## 8. Zusammenfassung
- **black** ist ein **sehr strenger, kaum konfigurierbarer Formatter** für Python-Code.
- Ziel:
- **einheitlicher Stil**,
- **keine Diskussionen** über Formatierung,
- Fokus auf Logik und Qualität statt auf Leerzeichen.
- Abgrenzung:
- black formatiert nur – es ist kein Linter und kein Typprüfer.
- Im Vergleich zu autopep8/yapf: weniger konfigurierbar, dafür klarer, einheitlicher Stil.
- Es löst:
- Stil-Inkonsistenz, Formatierungs-Diskussionen, „Rauschen“ in Diffs.
- Herausforderungen:
- Einstieg in ältere Projekte,
- wenig Einfluss auf Stil,
- Abstimmung mit anderen Tools, Versionsänderungen.
- In der Praxis:
- Nutze black im Editor (Format-on-save),
- im `pre-commit`-Hook,
- und als `--check` in der CI.
Wenn du möchtest, kann ich dir im nächsten Schritt ein kleines Beispielprojekt konstruieren (mit `pyproject.toml`, `pre-commit`, isort/black-Konfiguration), damit du siehst, wie man black von Anfang an sauber einrichtet.
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.
+1212
View File
File diff suppressed because it is too large Load Diff
Executable
+540
View File
@@ -0,0 +1,540 @@
Ruff ist ein relativ neues, sehr schnelles Tool zur Verbesserung der Code-Qualität in Python. Es kombiniert Funktionen, für die man früher mehrere Werkzeuge brauchte (Linter, Formatter, Import-Sortierer, teilweise Sicherheits-Checks), in einem Programm.
Ich gehe Schritt für Schritt durch:
1. Grundidee und Zielgruppe
2. Was macht ruff konkret?
3. Abgrenzung zu ähnlichen Tools
4. Welche Probleme löst ruff?
5. Typische Herausforderungen im Einsatz
6. Praxisnahe Beispiele (inkl. Konfiguration)
---
## 1. Grundidee von ruff – in einfachen Worten
Stell dir vor, mehrere Entwickler:innen arbeiten an einem Python-Projekt. Ohne Regeln sieht der Code schnell sehr unterschiedlich aus:
- Mal sind Leerzeichen anders gesetzt,
- mal werden nicht genutzte Variablen liegen gelassen,
- vielleicht wird an manchen Stellen unsicherer Code geschrieben,
- im schlimmsten Fall fallen bestimmte Fehler erst spät auf.
Bisher nutzte man typischerweise mehrere Tools:
- ein **Linter** (z. B. *flake8* oder *pylint*),
- einen **Formatter** (z. B. *black*),
- einen **Import-Sortierer** (z. B. *isort*),
- evtl. ein **Security-Tool** (z. B. *bandit*),
- plus diverse Plugins.
**Ruff will das bündeln:**
- Ein Tool für viele Code-Qualitätsaspekte
- Sehr schnell (geschrieben in Rust, nicht in Python)
- Einfache Konfiguration (alles typischerweise in `pyproject.toml`)
- Viele Checks, die sich an bekannten Tools orientieren (flake8-Plugins, isort-Regeln, bandit-Regeln usw.)
---
## 2. Was macht ruff konkret?
### 2.1. Linting (Fehler und Stilprobleme finden)
Ruff untersucht deinen Python-Code und meldet z. B.:
- Syntax-Fehler
- Ungenutzte Variablen oder Importe
- Logische Stolperfallen (z. B. `== None` statt `is None`)
- Stil-Regeln (z. B. maximale Zeilenlänge)
- Potenziell unsichere Konstruktionen
Beispiel:
```python
def calculate(a, b):
result = a + b
unused = 42
if a == None:
print("a is None")
return result
```
Ruff würde in etwa melden (vereinfacht):
- `unused` wird nie verwendet
- `a == None` – besser `a is None`
Viele dieser Probleme kann ruff auch **automatisch beheben** (`--fix`).
---
### 2.2. Formatierung (Code automatisch „schön“ machen)
Ruff hat inzwischen einen **eigenen Formatter**, der ähnlich wie *black* funktioniert:
- Einheitliche Einrückungen und Zeilenumbrüche
- Klammer-Formatierung
- Konsistente Verwendung von Anführungszeichen (je nach Einstellung)
- Entfernung überflüssiger Leerzeilen usw.
Beispiel (unformatierter Code):
```python
def foo( x:int,y:int )->int:
return x+y
```
Nach `ruff format` könnte das so aussehen:
```python
def foo(x: int, y: int) -> int:
return x + y
```
---
### 2.3. Imports sortieren und aufräumen
Ruff kann ähnlich wie *isort*:
- Importe sortieren (alphabetisch und nach Gruppen: Standardbibliothek, Drittanbieter, Projektcode)
- Unbenutzte Importe entfernen
Beispiel:
```python
import myproject.utils
import os
import sys
import requests
from math import sqrt
from math import ceil
```
Nach ruff (vereinfacht):
```python
import os
import sys
from math import ceil, sqrt
import requests
import myproject.utils
```
Und wenn `sys` gar nicht verwendet wird, kann ruff es auch entfernen.
---
### 2.4. Ein Tool – mehrere Regel-Sammlungen
Ruff bringt rule sets mit, die vielen bekannten Tools entsprechen, z. B.:
- **E/F/W**: Pycodestyle/Pyflakes-ähnlich (via flake8)
- **I**: isort-Regeln
- **N**: pep8-naming (Namenskonventionen)
- **S**: bandit (Sicherheitsregeln)
- **UP**: pyupgrade (veraltete Syntax, modernisieren)
- u. v. m.
Du kannst über die Konfiguration steuern, welche Regel-Gruppen du aktivierst oder deaktivierst.
---
## 3. Abgrenzung zu ähnlichen Tools
### 3.1. Ruff vs. flake8 (+ Plugins)
**flake8** ist ein Linter; für viele Extras braucht man Plugins:
- z. B. `flake8-bugbear`, `flake8-import-order`, `pep8-naming` usw.
**Ruff:**
- Bietet die Funktionalität vieler flake8-Plugins „eingebaut“.
- Ist deutlich **schneller** (insbesondere bei großen Projekten).
- Wird oft als Drop-in-Ersatz für flake8 verwendet.
Aber:
- flake8 ist schon lange etabliert; manche Teams haben stark angepasste flake8-Setups, die man nicht 1:1 nach ruff übertragen kann.
- Einige Spezial-Plugins existieren ggf. nur für flake8.
---
### 3.2. Ruff vs. pylint
**pylint**:
- Sehr umfangreicher Linter (viele komplexe Regeln, u. a. über Projektstruktur, OOP-Patterns usw.)
- Langsam im Vergleich zu ruff
- Detaillierte Reports und Scores
**Ruff**:
- Fokus auf **Geschwindigkeit** und auf Regeln, die sich gut automatisieren/auto-fixen lassen.
- Viele „klassische“ Lint-Regeln, Naming, Imports, Security-Basics, aber nicht alle tiefgehenden Analysen von pylint.
- Für komplexe Architektur-Regeln wird weiterhin oft pylint oder andere Tools genutzt.
---
### 3.3. Ruff vs. black
**black**:
- Reiner **Formatter** – macht nur Formatierung, keine Lint-Fehler (bis auf ganz wenige Ausnahmen).
- Sehr stabile, strikte Formatierung (Meinung: „The uncompromising code formatter“).
**Ruff**:
- Eigenständiger Formatter, der in vielen Projekten Black ersetzen kann.
- Zusätzlich: Linting, Import-Sortierung, Security-Regeln usw.
- Du kannst:
- nur ruff als Formatter nutzen, oder
- ruff als Linter + black als Formatter (dann `ruff format` nicht verwenden), wenn dein Team bereits stark auf black setzt.
---
### 3.4. Ruff vs. isort
**isort**:
- Spezialisiert auf das Sortieren von Imports.
**Ruff**:
- Hat eine integrierte Import-Sortierung (Regelgruppe `I`).
- Für die meisten Fälle reicht ruff völlig aus.
- Wenn ihr sehr spezielle Import-Sortierregeln braucht, ist isort manchmal noch flexibler, aber das wird immer weniger relevant.
---
### 3.5. Ruff vs. mypy/pyright (Typprüfung)
**mypy/pyright**:
- Statische Typprüfer: sie prüfen, ob die Typannotationen sinnvoll zusammenpassen.
- Finden z. B. Fehler wie: „Funktion gibt laut Typ `str` zurück, tatsächlich aber `int`“.
**Ruff**:
- Enthält Regeln, die mit Typannotationen arbeiten (z. B. Style, Safety), aber **kein vollwertiger Typprüfer**.
- Typfehler (im Sinne von mypy) sollten weiterhin mit mypy oder pyright geprüft werden.
---
### 3.6. Ruff vs. bandit (Security)
**bandit**:
- Spezialisiertes Security-Tool für Python.
**Ruff**:
- Hat viele bandit-Regeln integriert (Regelgruppe `S`).
- Deckt gängige Sicherheitsfallen ab (z. B. `eval` auf untrusted Input, hartkodierte Passwörter etc.).
- Für tiefgehende Security-Audits kann ein spezialisiertes Tool trotzdem sinnvoll sein.
---
## 4. Welche Probleme löst ruff?
### 4.1. Performance-Probleme in großen Projekten
Früher:
- flake8 + black + isort + bandit + mypy
→ viele Tools, mehrfaches Einlesen des Codes, CI dauert lange.
Mit ruff:
- Ein Tool übernimmt Linting, Formatierung, Importe, einen großen Teil der Security-Regeln.
- Deutlich weniger Laufzeit, besonders in CI-Pipelines oder bei großen Repositories.
---
### 4.2. Zu viele Tools, komplizierte Konfiguration
Problem:
- Unterschiedliche Konfigurationsdateien (`.flake8`, `pyproject.toml`, `setup.cfg`, `.isort.cfg`, `pyproject.toml` für black…)
- Mehr Aufwand beim Onboarding neuer Teammitglieder.
Ruff:
- Typischerweise alles in **einer** Konfiguration (`pyproject.toml`).
- Weniger bewegliche Teile, einfachere Wartung.
---
### 4.3. Inkonsequente Codequalität im Team
Ohne einheitliche Tools:
- Jede:r schreibt etwas anders.
- Diskussionen in Code-Reviews drehen sich um Stil statt Inhalte.
- Fehler (z. B. ungenutzte Variablen, potentielle Bugs) werden erst spät bemerkt.
Mit ruff:
- Gemeinsame, automatisierte Regeln.
- Automatische Fixes für vieles (z. B. im Editor oder Pre-Commit-Hooks).
- Code-Reviews können sich auf Architektur und Logik konzentrieren.
---
### 4.4. Technische Schulden reduzieren
In älteren Projekten:
- Viele kleine Stil- und Qualitätsprobleme haben sich angesammelt.
- Niemand möchte „alles mal eben aufräumen“, weil Tools zu langsam sind oder es zu viel ist.
Mit ruff:
- Durch die Geschwindigkeit kann man auch große Codebasen lintern.
- Schrittweise Verbesserung möglich: z. B. zunächst nur wichtige Regelgruppen aktivieren, später mehr.
---
## 5. Herausforderungen bei der Nutzung von ruff
### 5.1. Zu viele Meldungen am Anfang
Wenn du ruff das erste Mal auf ein älteres Projekt loslässt, bekommst du oft hunderte oder tausende Meldungen.
Strategie:
- Nur einen Teil der Regeln aktivieren (z. B. nur „kritische“ oder klar hilfreiche).
- Bestehende Verstöße einmalig ignorieren (per `--ignore` oder `--per-file-ignores`) und neue Verstöße blocken.
- Nach und nach alte Stellen aufräumen.
---
### 5.2. Regeln verstehen und anpassen
Ruff hat sehr viele Regeln. Nicht alle passen zu jedem Projekt.
- Manche Regeln sind sehr streng (z. B. bestimmte Naming- oder Docstring-Regeln).
- Du musst überlegen: Welche Regeln sind für unser Team sinnvoll?
Lösung:
- Regeln gezielt aktivieren/deaktivieren.
- Dokumentieren, warum bestimmte Regeln aktiv oder abgeschaltet sind.
---
### 5.3. Wechsel von bestehender Tool-Landschaft
Wenn ihr schon flake8, black, isort etc. nutzt:
- Müssen Einstellungen in ruff nachgebaut werden.
- Manche Teams werden black nicht sofort durch ruff format ersetzen wollen.
Ein pragmatischer Weg:
1. Zuerst ruff als **Linter** einführen (Formatierung bleibt bei black).
2. Wenn gewünscht, später ruff format testen und ggf. black ersetzen.
---
### 5.4. Auto-Fixes mit Vorsicht genießen
Ruff kann sehr viel automatisch reparieren:
- In der Regel gut, aber:
- Bei manchen Regeln sollte man prüfen, ob die Änderung wirklich die Absicht trifft.
- In kritischen Bereichen (z. B. Security-sensible Logik) ggf. Auto-Fixes nicht blind akzeptieren.
Empfehlung:
- Auto-Fixes lokal ausführen, dann diff anschauen.
- In CI eher nur prüfen, nicht fixen.
---
### 5.5. Editor-Integration
Ruff hat gute Unterstützung in vielen Editoren (VS Code, PyCharm, Neovim usw.), aber:
- Man muss oft ein Plugin oder eine Extension installieren.
- Manchmal überschneidet sich das mit vorhandenen Tools (z. B. Black-Extension vs. Ruff-Formatter); das muss sauber konfiguriert werden.
---
## 6. Praxisnahe Beispiele
### 6.1. Installation von ruff
Typisch über `pip`:
```bash
pip install ruff
```
Oder als dev-Abhängigkeit in `pyproject.toml` (Poetry/uv/pip-tools etc.).
---
### 6.2. Einfacher Aufruf
Linting:
```bash
ruff check .
```
Mit automatischen Fixes:
```bash
ruff check . --fix
```
Formatierung:
```bash
ruff format .
```
---
### 6.3. Minimale Konfiguration in `pyproject.toml`
```toml
[tool.ruff]
target-version = "py311" # welche Python-Version ihr nutzt
line-length = 88 # maximale Zeilenlänge
exclude = ["venv", ".venv", "build", "dist"]
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"] # Regelgruppen: Errors, Flake, Imports, Bugbear, pyupgrade
ignore = ["E501"] # Beispiel: lange Zeilen ignoriere ich (wenn ich black nutze)
[tool.ruff.format]
# Beispiel: wenn ihr black-kompatible Einstellungen wollt
quote-style = "double"
indent-style = "space"
line-ending = "auto"
```
- `select` gibt an, welche Regelgruppen aktiv sind.
- `ignore` hebt einzelne Regelcodes auf (z. B. `E501` für Zeilenlänge).
---
### 6.4. Beispiel: Unbenutzte Variable und schlechtes `None`-Checking
Code:
```python
def process(items):
count = len(items)
unused = 0
if items == None:
print("No items")
return count
```
Ruff meldet u. a.:
- `F841` (unused variable `unused`)
- `E711` (Vergleich mit `None` mit `==` statt `is`)
Mit `ruff check . --fix` könnte daraus werden:
```python
def process(items):
count = len(items)
if items is None:
print("No items")
return count
```
---
### 6.5. Beispiel: Imports aufräumen
Code:
```python
import os
import sys
import json
import requests
def get_data(url):
r = requests.get(url)
return r.json()
```
Hier wird `os`, `sys` und `json` nicht genutzt.
Ruff meldet:
- `F401` (unused imports: os, sys, json)
Nach `ruff check . --fix`:
```python
import requests
def get_data(url):
r = requests.get(url)
return r.json()
```
---
### 6.6. Beispiel: Einsatz mit pre-commit
Viele Teams nutzen [pre-commit](https://pre-commit.com/), damit Checks automatisch vor jedem Git-Commit laufen.
`.pre-commit-config.yaml`:
```yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.5 # Beispielversion, ggf. aktualisieren
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
```
Damit werden:
- Linting + Auto-Fix vor jedem Commit ausgeführt,
- Formatierung automatisch angewendet.
---
## 7. Zusammenfassung
- **Ruff** ist ein modernes, sehr schnelles Tool zur Verbesserung der Code-Qualität in Python.
- Es kombiniert:
- Linting (Fehler und Stil),
- Formatierung,
- Import-Sortierung,
- viele Sicherheits- und Modernisierungsregeln.
- Es ersetzt in vielen Projekten:
- flake8 (+ viele Plugins),
- isort,
- teilweise black,
- teilweise bandit.
- Es löst Probleme wie:
- lange Tool-Laufzeiten,
- fragmentierte Konfiguration,
- inkonsistente Codequalität.
- Herausforderungen sind v. a.:
- Auswahl und Anpassung der Regeln,
- Umstieg von bestehenden Tools,
- Umgang mit großen Mengen an Anfangs-Warnungen.
Wenn du möchtest, kann ich dir im nächsten Schritt eine **konkrete Beispielkonfiguration für ein kleines oder ein großes Projekt** vorschlagen (inkl. welche Regelgruppen sinnvoll sind und welche man am Anfang lieber auslässt).
Executable
+94
View File
@@ -0,0 +1,94 @@
`uv` ist ein neues, sehr schnelles All-in-one-Tool für das Python-Ökosystem, entwickelt von Astral (den Machern von `ruff`).
Es ist im Kern ein **extrem schneller Paket- und Environment-Manager**, der mehrere bekannte Tools ersetzt bzw. bündelt:
- `pip` / `pip-tools` (Installation & Lockfiles)
- `virtualenv` / `venv` (virtuelle Umgebungen)
- `pyenv` (verschiedene Python-Versionen verwalten)
- Teile von `poetry` / `pipx` (Projekt- & Tool-Management)
`pip install uv` installiert dabei nur einen Bootstrapper, der das eigentliche `uv`-Binary nachlädt.
---
## Wofür ist `uv` gut?
### 1. Pakete extrem schnell installieren
`uv` ist in Rust geschrieben und deutlich schneller als `pip` oder `pip-tools` beim Auflösen und Installieren von Abhängigkeiten.
Beispiel (Drop-in für pip):
```bash
uv pip install requests
```
Auch mit `requirements.txt`:
```bash
uv pip install -r requirements.txt
```
---
### 2. Virtuelle Umgebungen verwalten
```bash
# Neue venv erstellen
uv venv .venv
# venv aktivieren (z. B. in bash)
source .venv/bin/activate
```
---
### 3. Python-Versionen verwalten
Ähnlich wie `pyenv`:
```bash
# Bestimmte Python-Version installieren
uv python install 3.12
# Projekt mit bestimmter Version starten
uv run --python 3.12 main.py
```
---
### 4. Projekte initialisieren & reproduzierbare Builds
`uv` unterstützt `pyproject.toml` und Lockfiles (`uv.lock`) für reproduzierbare Umgebungen:
```bash
# Neues Projekt anlegen
uv init my-project
cd my-project
uv sync # Installiert Abhängigkeiten gemäß pyproject + uv.lock
```
---
### 5. Skripte/Tools direkt ausführen
Ähnlich wie `pipx`:
```bash
uv run black .
uv run pytest
```
`uv` kümmert sich im Hintergrund um die passende Umgebung und Abhängigkeiten.
---
## Kurzfassung
`uv` ist sinnvoll, wenn du:
- Python-Pakete **deutlich schneller** installieren willst,
- **alles rund um Python-Umgebungen** (Packages, venvs, Python-Versionen, Tools) mit **einem** konsistenten Tool verwalten möchtest,
- reproduzierbare Builds mit `pyproject.toml` und Lockfile brauchst,
- eine moderne Alternative zu `pip`, `virtualenv`, `pyenv`, `pip-tools`, teilweise `poetry`/`pipx` suchst.
Wenn du aktuell `pip`, `virtualenv` & Co nutzt, kannst du mittelfristig sehr viel davon durch `uv` ersetzen.
+469
View File
@@ -0,0 +1,469 @@
## 1. Grundsätzliche Definition: Was ist **uvicorn**?
**uvicorn** ist ein extrem performanter, asynchroner Web-Server für Python, der das **ASGI**‑Protokoll unterstützt.
- **ASGI** = *Asynchronous Server Gateway Interface*
- uvicorn ist also das Bindeglied zwischen:
- dem Web (HTTP, WebSockets)
- und deiner Python‑Applikation (z.B. [[FastAPI]], Starlette, Django mit ASGI)
Uvicorn basiert intern auf sehr schnellen C‑Bibliotheken:
- **uvloop** (schneller Event Loop, Ersatz für `asyncio`‑Loop)
- **httptools** (schnelles HTTP‑Parsing)
Du verwendest uvicorn typischerweise, um eine ASGI‑App „zu starten“:
```bash
uvicorn main:app --reload
```
---
## 2. Wichtige Begriffe: ASGI, WSGI und Web-Frameworks
### 2.1 ASGI vs. WSGI
- **WSGI** (älterer Standard, z.B. für Django (klassisch), Flask):
- synchron
- kein natives WebSocket‑Support
- typische Server: `gunicorn`, `uWSGI`, `mod_wsgi`
- **ASGI** (moderner Standard):
- unterstützt **async/await**
- kann **HTTP** und **WebSockets** und Background Tasks
- typische Server: `uvicorn`, `hypercorn`, `daphne`
uvicorn ist also ein **ASGI-Server**, nicht WSGI.
### 2.2 uvicorn vs. Web-Frameworks ([[FastAPI]], Starlette, Django, Flask)
- **Framework** ([[FastAPI]], Starlette, Django, Flask):
- definiert, wie du Routen, Views, Models, etc. schreibst.
- kümmert sich um Request/Response‑Logik
- **uvicorn**:
- kümmert sich um das Annehmen von Verbindungen, HTTP‑Parsing, Event‑Loop‑Handling.
- ruft deine Applikation nur gemäß dem ASGI‑Protokoll auf.
Bildlich:
**Browser** → (HTTP) → **uvicorn** → (ASGI) → **deine App** (z.B. [[FastAPI]])
---
## 3. Abgrenzung zu ähnlichen oder verwandten Begriffen
### 3.1 uvicorn vs. Gunicorn
- **gunicorn**:
- klassischer **WSGI**-Server (für sync‑Apps wie Flask oder Django ohne ASGI).
- kann aber mithilfe von Workern wie `uvicorn.workers.UvicornWorker` auch ASGI-App starten.
Beispiel: [[FastAPI]]‑App mit gunicorn + uvicorn worker:
```bash
gunicorn -k uvicorn.workers.UvicornWorker main:app -b 0.0.0.0:8000
```
Hier ist:
- gunicorn = Prozessmanager und Worker‑Spawner
- uvicorn = eigentlicher ASGI‑Server pro Worker
### 3.2 uvicorn vs. Hypercorn / Daphne
- **hypercorn**:
- anderer ASGI‑Server (unterstützt z.B. HTTP/2, verschiedene Event Loops)
- **daphne**:
- ASGI‑Server aus dem Django‑Channels‑Ökosystem
Alle drei (uvicorn, hypercorn, daphne) machen im Kern das Gleiche:
**ASGI‑Apps ausführen**, unterscheiden sich aber in Features, Performance und Konfigurationsmöglichkeiten.
### 3.3 uvicorn vs. „eingebauter Development-Server“
Viele Frameworks haben eingebaute Dev-Server, z.B.:
- Flask: `app.run(debug=True)`
- Django: `python manage.py runserver`
Diese sind:
- für **Entwicklung** gedacht
- nicht für **Produktion** (Performance, Stabilität, Security)
uvicorn ist ein **richtiger** Webserver, der für **Produktion** geeignet ist (oft zusammen mit einem Reverse Proxy wie [[Nginx]]).
---
## 4. Welche Probleme löst uvicorn?
### 4.1 Asynchrone Web‑Backends performant betreiben
Mit ASGI kannst du:
- `async def` Endpoints schreiben
- WebSockets nutzen
- viele gleichzeitige Requests mit einem Event Loop bedienen
uvicorn ermöglicht dir, diese **asynchronen** Apps performant auszuliefern.
Praxisnahes Beispiel ([[FastAPI]]):
```python
# main.py
from fastapi import FastAPI
import asyncio
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
await asyncio.sleep(1) # simuliert eine I/O-Operation
return {"item_id": item_id}
```
Starten mit uvicorn:
```bash
uvicorn main:app --reload
```
uvicorn kümmert sich darum, dass mehrere Requests gleichzeitig abgearbeitet werden können, während `asyncio.sleep` nicht blockiert.
### 4.2 WebSockets und Long-Lived Connections
ASGI (und damit uvicorn) unterstützt **WebSockets** nativ, was mit WSGI nicht geht.
Beispiel mit Starlette:
```python
# main.py
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.websockets import WebSocket
from starlette.routing import Route, WebSocketRoute
async def homepage(request):
return JSONResponse({"hello": "world"})
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
await websocket.send_text("Willkommen!")
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Du hast gesendet: {data}")
routes = [
Route("/", endpoint=homepage),
WebSocketRoute("/ws", endpoint=websocket_endpoint),
]
app = Starlette(routes=routes)
```
Start:
```bash
uvicorn main:app
```
Mit WSGI wäre so ein WebSocket‑Endpoint nicht möglich.
### 4.3 Produktionstauglicher Server gegenüber Entwicklungsservern
- Stabilität bei hoher Last
- Steuerung von:
- Anzahl Worker‑Prozesse
- Timeouts
- Logging
- Start via CLI, systemd, Docker, Kubernetes etc.
---
## 5. Grundlegende Verwendung von uvicorn
### 5.1 Installation
```bash
pip install uvicorn
# optional: schnellere Variante mit C-Extensions
pip install "uvicorn[standard]"
```
`[standard]` installiert u.a. `uvloop` und `httptools`.
### 5.2 Minimalbeispiel: Plain-ASGI-App
Du kannst eine ASGI‑App auch ohne Framework schreiben:
```python
# app.py
async def app(scope, receive, send):
assert scope["type"] == "http"
# Request body lesen (vereinfachter Fall)
await receive()
body = b"Hello, world"
headers = [(b"content-type", b"text/plain")]
await send({
"type": "http.response.start",
"status": 200,
"headers": headers,
})
await send({
"type": "http.response.body",
"body": body,
})
```
Starten:
```bash
uvicorn app:app --reload
```
Erklärung:
- `app:app` = Modul `app.py`, Variable `app`
- `--reload` = automatischer Neustart bei Codeänderung (nur dev)
### 5.3 Beispiel mit [[FastAPI]]
```python
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello from uvicorn + FastAPI"}
```
Start:
```bash
uvicorn main:app --reload --host 0.0.0.0 --port 8000
```
Wichtige CLI‑Optionen:
- `--reload`: Auto-Reload bei Codeänderungen (Dev)
- `--host`: z.B. `0.0.0.0` um von außen erreichbar zu sein
- `--port`: Port, z.B. `8000`
- `--workers`: Anzahl der Prozesse (für Produktion)
### 5.4 Starten aus Python heraus
```python
# run.py
import uvicorn
if __name__ == "__main__":
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8000,
reload=True,
)
```
Start:
```bash
python run.py
```
---
## 6. Typische Konfigurationen und Szenarien
### 6.1 Entwicklung
- ein Worker
- `--reload` aktiviert
- Logging auf `debug`
```bash
uvicorn main:app --reload --host 0.0.0.0 --port 8000 --log-level debug
```
### 6.2 Produktion (einfach)
- mehrere Worker-Prozesse
- kein `--reload`
- Logging eher `info` oder `warning`
```bash
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 --log-level info
```
Richtwert für Worker:
`Anzahl CPU-Kerne * 2` (abhängig von App und Last; immer testen).
### 6.3 Produktion hinter einem Reverse Proxy (z.B. [[Nginx]])
Typischer Aufbau:
```
Internet → Nginx (TLS, gzip, etc.) → uvicorn → FastAPI/Starlette/Django
```
- [[Nginx]] übernimmt TLS/SSL, Load Balancing, Static Files
- uvicorn macht die Application-Logik
[[Nginx]]-Konfig (stark vereinfacht) könnte so aussehen:
```nginx
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
```
uvicorn wird weiterhin wie oben gestartet.
---
## 7. Herausforderungen und typische Stolpersteine
### 7.1 Async/Synchron-Mix und Blockierungen
**Problem:**
Du verwendest uvicorn (ASGI, async), aber in deinen Endpoints gibt es blockierende Operationen:
- große CPU‑Aufgaben
- synchrones Warten auf externe APIs (z.B. `requests.get(...)`)
- schwere Datenbank‑Queries, die nicht async sind
Beispiel:
```python
@app.get("/slow")
async def slow():
import time
time.sleep(5) # BLOCKIERT den Event-Loop
return {"status": "ok"}
```
Folge:
- Ein Request blockiert den Event Loop → alle anderen Requests warten mit.
Lösungen:
- I/O: async Libraries benutzen (z.B. `httpx` statt `requests`, `asyncpg` statt sync‑DB‑Client)
- CPU‑lastig: in Thread‑Pool oder Process‑Pool auslagern (`run_in_threadpool` etc.)
### 7.2 Gemeinsamer Zustand über Worker-Prozesse
Wenn du `--workers > 1` nutzt, hast du **mehrere Prozesse**.
Globaler Zustand in Python wird **nicht** zwischen Prozessen geteilt.
Beispiel (Problem):
```python
counter = 0
@app.get("/count")
def count():
global counter
counter += 1
return {"counter": counter}
```
Mit mehreren Workern:
- jeder Worker hat seinen eigenen `counter`
- Ergebnisse sind inkonsistent
Lösung:
- geteilten Zustand über externe Systeme (Redis, Datenbank, etc.)
- oder nur einen Worker nutzen, wenn globaler In-Memory-State unvermeidbar ist (aber meist unsauber).
### 7.3 Datenbankverbindungen und Lebenszyklus
uvicorn unterstützt ASGI‑`lifespan`‑Events (startup/shutdown).
Frameworks wie [[FastAPI]]/Starlette nutzen das, um z.B. DB‑Connections zu öffnen/schließen.
Stolpersteine:
- Verbindungspools pro Worker korrekt initialisieren
- bei Shutdown sauber schließen
- nicht „pro Request“ neue Connections aufmachen
Beispiel mit [[FastAPI]] (vereinfacht):
```python
from fastapi import FastAPI
app = FastAPI()
db = None
@app.on_event("startup")
async def startup():
global db
db = await some_async_db_connect()
@app.on_event("shutdown")
async def shutdown():
await db.close()
```
### 7.4 Logging und Error-Handling
uvicorn hat eigenes Logging; dein Framework ebenso.
Typische Themen:
- Log-Format in Produktion standardisieren
- Fehler-Logs im Zusammenspiel mit Reverse Proxy
- Ausführliche Logs in Dev, weniger in Prod
Beispiel (JSON-Logging in Produktion, nur angedeutet):
```bash
uvicorn main:app \
--host 0.0.0.0 \
--port 8000 \
--log-config logging_config.yaml
```
In `logging_config.yaml` kannst du detailliert das Logging steuern.
### 7.5 Plattformunterschiede (Windows vs. Linux)
- `--reload` nutzt File-Watcher und Signale → unter Linux sehr stabil; unter Windows kann es ein paar Besonderheiten geben.
- In Produktion läuft uvicorn meist auf Linux‑Servern oder in Docker‑Containern.
---
## 8. Kurze Checkliste für den praktischen Einstieg
1. **Framework wählen**
- [[FastAPI]] oder Starlette, wenn du intensiv async nutzen willst.
1. **App schreiben**
- `app = FastAPI()`, Endpoints definieren.
3. **In Entwicklung starten**
```bash
uvicorn main:app --reload
```
4. **Vor Produktion**
- Blocking‑Code prüfen (CPU, I/O)
- Datenbankzugriff sauber konfigurieren (Pools, async‑Client)
- Logging und Error‑Handling aufräumen
5. **In Produktion starten** (einfach)
```bash
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
```
6. **Optional**:
- vor uvicorn einen [[Nginx]] oder Traefik setzen (TLS, Load Balancing)
---
## 9. Zusammenfassung
- **uvicorn** ist ein **ASGI‑Webserver** für Python, optimiert für **asynchrone** Web‑Apps.
- Er ist **kein Web‑Framework**, sondern die Laufzeitumgebung für Frameworks wie **[[FastAPI]]**, **Starlette** oder moderne **Django**‑Konfigurationen.
- Es löst die Probleme klassischer WSGI‑Server in Bezug auf **Async**, **WebSockets** und **Performance**.
- Typische Herausforderungen liegen im Bereich:
- korrektes Async‑Design
- Umgang mit mehreren Workern und gemeinsamem Zustand
- saubere Integration von Datenbanken, Logging, Deployment
- Für dich als Python‑Entwickler ist uvicorn im Alltag vor allem:
**das Kommando, mit dem du deine moderne Web‑API startest.**