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