Files
work/notes/nextcloud/Notes/IT-Know-How/python/Pydantic.md
T

470 lines
13 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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).