init at work
This commit is contained in:
Executable
+470
@@ -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).
|
||||
Reference in New Issue
Block a user