470 lines
13 KiB
Markdown
Executable File
470 lines
13 KiB
Markdown
Executable File
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). |