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

13 KiB
Executable File
Raw Blame History

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:

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:

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“:

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:

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:

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:

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:

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:

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.
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:

from pydantic import BaseModel, StrictInt

class Model(BaseModel):
    value: StrictInt

# Model(value="1") -> ValidationError (keine Autokonvertierung)

Oder über model_config:

class Model(BaseModel):
    value: int

    model_config = {
        "strict": True,
    }

5.3 Umgang mit Optional, Defaults, Required

Typische Stolperfallen:

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_asTypeAdapter.
  • 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

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

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

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:

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).