add synced notes on IT know how

This commit is contained in:
Mathias Schneider
2026-03-17 19:01:50 +01:00
parent b8797d5ca8
commit fc3aef4da8
31 changed files with 13162 additions and 1 deletions
@@ -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]]/Lets 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).
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.
+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.
File diff suppressed because it is too large Load Diff
+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).
+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 PythonApplikation (z.B. [[FastAPI]], Starlette, Django mit ASGI)
Uvicorn basiert intern auf sehr schnellen CBibliotheken:
- **uvloop** (schneller Event Loop, Ersatz für `asyncio`Loop)
- **httptools** (schnelles HTTPParsing)
Du verwendest uvicorn typischerweise, um eine ASGIApp „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 WebSocketSupport
- 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/ResponseLogik
- **uvicorn**:
- kümmert sich um das Annehmen von Verbindungen, HTTPParsing, EventLoopHandling.
- ruft deine Applikation nur gemäß dem ASGIProtokoll 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 syncApps 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 WorkerSpawner
- uvicorn = eigentlicher ASGIServer pro Worker
### 3.2 uvicorn vs. Hypercorn / Daphne
- **hypercorn**:
- anderer ASGIServer (unterstützt z.B. HTTP/2, verschiedene Event Loops)
- **daphne**:
- ASGIServer aus dem DjangoChannels‑Ökosystem
Alle drei (uvicorn, hypercorn, daphne) machen im Kern das Gleiche:
**ASGIApps 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 WebBackends 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 WebSocketEndpoint nicht möglich.
### 4.3 Produktionstauglicher Server gegenüber Entwicklungsservern
- Stabilität bei hoher Last
- Steuerung von:
- Anzahl WorkerProzesse
- 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 ASGIApp 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 CLIOptionen:
- `--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 CPUAufgaben
- synchrones Warten auf externe APIs (z.B. `requests.get(...)`)
- schwere DatenbankQueries, 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 syncDBClient)
- CPUlastig: in ThreadPool oder ProcessPool 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. DBConnections 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 LinuxServern oder in DockerContainern.
---
## 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**
- BlockingCode prüfen (CPU, I/O)
- Datenbankzugriff sauber konfigurieren (Pools, asyncClient)
- Logging und ErrorHandling 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 **ASGIWebserver** für Python, optimiert für **asynchrone** WebApps.
- Er ist **kein WebFramework**, sondern die Laufzeitumgebung für Frameworks wie **[[FastAPI]]**, **Starlette** oder moderne **Django**Konfigurationen.
- Es löst die Probleme klassischer WSGIServer in Bezug auf **Async**, **WebSockets** und **Performance**.
- Typische Herausforderungen liegen im Bereich:
- korrektes AsyncDesign
- Umgang mit mehreren Workern und gemeinsamem Zustand
- saubere Integration von Datenbanken, Logging, Deployment
- Für dich als PythonEntwickler ist uvicorn im Alltag vor allem:
**das Kommando, mit dem du deine moderne WebAPI startest.**