Pydantic v2: Eingabedaten in Python validieren
1. Übersicht
Daten aus HTTP-Requests, JSON-Dateien oder Message Queues haben häufig falsche Typen, fehlende Felder oder unsinnige Werte. Wenn du mit verstreuten if/else-Anweisungen prüfst, wird der Code schnell unordentlich. Pydantic v2 ermöglicht es dir, die „Form” von Daten mit Type-Hints zu beschreiben und dann automatisch zu parsen, Typen zu erzwingen und detaillierte Fehler zu melden.
In diesem Artikel gehen wir durch die wichtigsten Tools von Pydantic v2: BaseModel, Field, field_validator, model_validator, ConfigDict, TypeAdapter. Im letzten Teil erfährst du, wie du mit pytest Tests schreibst. Nach diesem Artikel wirst du in der Lage sein, sichere Validierungsmodelle für deine Anwendung selbst zu erstellen.
2. Installation
Dieser Artikel verwendet Python 3.11+ und Pydantic 2.x:
pip install "pydantic>=2.7" pytest
Wenn du den Typ EmailStr benötigst, installiere zusätzlich das Erweiterungspaket:
pip install "pydantic[email]"
3. Grundlegendes Modell mit BaseModel
3.1. Deklaration und Typkonvertierung
Ein Modell ist eine Klasse, die BaseModel erbt. Jedes Attribut mit einem Type-Hint ist ein Feld:
from pydantic import BaseModel
class Customer(BaseModel):
id: int
name: str
email: str
is_active: bool = True
customer = Customer(id="42", name="An", email="an@example.com")
print(customer)
print(customer.id, type(customer.id))
id=42 name='An' email='an@example.com' is_active=True
42 <class 'int'>
Du übergibst id="42" als String, aber Pydantic konvertiert es automatisch zu int. Das ist der Lax-Modus, der Standardmodus: Pydantic erzwingt Typkonvertierungen, wenn die Umwandlung sinnvoll ist. Das Feld is_active hat einen Standardwert, daher ist es nicht erforderlich, es zu übergeben.
3.2. Umgang mit ValidationError
Wenn Daten ungültig sind, wirft Pydantic einen ValidationError und sammelt alle Fehler auf einmal, nicht nur den ersten:
from pydantic import ValidationError
try:
Customer(id="abc", name="An")
except ValidationError as e:
print(e.error_count())
for err in e.errors():
print(err["loc"], err["type"], err["msg"])
2
('id',) int_parsing Input should be a valid integer, unable to parse string as an integer
('email',) missing Field required
Jeder Fehler ist ein dict mit loc (Feldposition), type (stabiler Fehlercode) und msg (Meldung für den Leser). Wenn du einen Fehler an den Client zurückgibst, solltest du loc und type verwenden. Der Inhalt von msg kann sich zwischen Versionen ändern.
4. Wertbeschränkungen mit Field und Annotated
Type-Hints prüfen nur den Typ. Um Werte zu beschränken, verwendest du Field:
from decimal import Decimal
from typing import Annotated
from pydantic import BaseModel, Field
Quantity = Annotated[int, Field(gt=0, le=100)]
class OrderItem(BaseModel):
sku: str = Field(pattern=r"^[A-Z]{3}-\d{4}$")
quantity: Quantity
unit_price: Decimal = Field(gt=0, max_digits=10, decimal_places=2)
item = OrderItem(sku="ABC-1234", quantity=3, unit_price="19.90")
print(item)
sku='ABC-1234' quantity=3 unit_price=Decimal('19.90')
Erklärung:
patternerzwingt, dassskueinen Regex erfüllt, z. B.ABC-1234.Quantityist ein wiederverwendbarer Typ dankAnnotated. Jedes Feld, dasQuantitydeklariert, muss im Bereich(0, 100]liegen.unit_priceverwendetDecimal, um Gleitkommafehler zu vermeiden. Der Wert muss positiv sein und darf maximal 2 Dezimalstellen haben.
Versuchen wir, ungültige Daten zu übergeben:
try:
OrderItem(sku="abc", quantity=0, unit_price="10")
except ValidationError as e:
for err in e.errors():
print(err["loc"], err["type"])
('sku',) string_pattern_mismatch
('quantity',) greater_than
Weitere häufig verwendete Constraints: min_length/max_length für Strings und Listen, ge/lt für Zahlen, multiple_of für Vielfache.
5. Benutzerdefinierte Validatoren mit field_validator
Wenn Constraints komplexer sind als Regex oder Wertbereiche, schreibst du einen benutzerdefinierten Validator:
from pydantic import BaseModel, Field, field_validator
class Contact(BaseModel):
name: str
phone: str = Field(pattern=r"^0\d{9}$")
@field_validator("name")
@classmethod
def clean_name(cls, v: str) -> str:
v = v.strip()
if not v:
raise ValueError("name darf nicht leer sein")
return v.title()
@field_validator("phone", mode="before")
@classmethod
def normalize_phone(cls, v):
if isinstance(v, str):
v = v.replace(" ", "").replace(".", "")
if v.startswith("+84"):
v = "0" + v[3:]
return v
contact = Contact(name=" nguyễn văn an ", phone="+84 912 345 678")
print(contact)
name='Nguyễn Văn An' phone='0912345678'
Es gibt zwei Modi zu unterscheiden:
mode="after"(Standard), wieclean_name: läuft nach der Typprüfung durch Pydantic, daher istvgarantiert einstr.mode="before", wienormalize_phone: läuft vor der Typprüfung undpattern.vkann alles sein, daher musst duisinstanceüberprüfen. Hier normalisieren wir+84 912 345 678zu0912345678, dann wirdpatternangewendet.
Ein Validator muss einen Wert zurückgeben. Der zurückgegebene Wert ist der Wert, der im Feld gespeichert wird. Um einen Fehler zu melden, wirfst du ValueError oder AssertionError. Pydantic wickelt ihn als value_error Fehler ein, mit der Meldung "Value error, name darf nicht leer sein".
Hinweis: Die Reihenfolge der Decorator ist sehr wichtig.
@field_validatormuss über@classmethodstehen. Wenn du sie vertauschst, erhältst du beim Definieren der Klasse einen unverständlichen Fehler.
6. Mehrere Felder gleichzeitig mit model_validator validieren
Einige Regeln beziehen sich auf mehrere Felder, z. B. das Auschecken muss nach dem Einchecken sein. In diesem Fall benötigt man model_validator:
from datetime import date
from typing import Self
from pydantic import BaseModel, ValidationError, model_validator
class Booking(BaseModel):
room_id: int
check_in: date
check_out: date
@model_validator(mode="after")
def check_dates(self) -> Self:
if self.check_out <= self.check_in:
raise ValueError("check_out muss nach check_in sein")
return self
try:
Booking(room_id=101, check_in="2026-10-20", check_out="2026-10-18")
except ValidationError as e:
err = e.errors()[0]
print(err["loc"], err["msg"])
() Value error, check_out muss nach check_in sein
Mit mode="after" erhält der Validator eine Instanz, deren Felder bereits validiert sind. Du greifst auf self.check_in wie auf ein normales Attribut zu und musst return self verwenden. Der Fehler hat ein leeres loc (), weil er sich auf das gesamte Modell bezieht, nicht auf ein einzelnes Feld. Der String "2026-10-20" wird auch automatisch zu date geparst.
7. Modellkonfiguration mit ConfigDict
model_config kontrolliert das Gesamtverhalten des Modells. Dies ist eine beliebte Konfiguration für sensible Daten wie Zahlungen:
from pydantic import BaseModel, ConfigDict, ValidationError
class PaymentRequest(BaseModel):
model_config = ConfigDict(
extra="forbid",
strict=True,
str_strip_whitespace=True,
frozen=True,
)
order_id: int
amount: int
currency: str
try:
PaymentRequest(order_id="1", amount=50000, currency="VND", coupon="FREE")
except ValidationError as e:
for err in e.errors():
print(err["loc"], err["type"])
('order_id',) int_type
('coupon',) extra_forbidden
Bedeutung jeder Option:
extra="forbid": lehnt unbekannte Felder wiecouponab. Standard ist"ignore", d. h. Pydantic ignoriert stillschweigend zusätzliche Felder. Dies kann Fehler bei Tippfehlern in Feldnamen auf der Client-Seite verbergen.strict=True: deaktiviert Typkonvertierung."1"wird nicht mehr fürintakzeptiert.str_strip_whitespace=True: entfernt automatisch Leerzeichen am Anfang und Ende von Strings.frozen=True: Instanzen sind unveränderlich. Das Zuweisen vonpayment.amount = 0wirft einen Fehler.
Wenn du nur für einige Felder strict mode möchtest, verwendest du Field(strict=True) oder Typen wie StrictInt, StrictStr.
Eine weitere bemerkenswerte Option ist validate_assignment=True. Standardmäßig validiert Pydantic nicht, wenn du ein Attribut nach dem Erstellen des Objekts erneut zuweist. Aktiviere diese Option, um das Modell immer gültig zu halten.
8. Validierung von dict, JSON und beliebigen Typen
In der Praxis kommen Daten oft als dict oder JSON-String. Pydantic v2 bietet zwei Klassenmethoden:
raw_json = '{"id": 7, "name": "Bình", "email": "binh@example.com"}'
customer = Customer.model_validate_json(raw_json)
raw_dict = {"id": 8, "name": "Chi", "email": "chi@example.com", "is_active": False}
customer2 = Customer.model_validate(raw_dict)
print(customer.model_dump())
print(customer2.model_dump_json())
{'id': 7, 'name': 'Bình', 'email': 'binh@example.com', 'is_active': True}
{"id":8,"name":"Chi","email":"chi@example.com","is_active":false}
Wenn die Eingabe ein JSON-String ist, solltest du model_validate_json verwenden, anstatt json.loads gefolgt von model_validate zu nutzen. Dies ist schneller, da Pydantics Rust-Kern Parsing und Validierung in einem Schritt durchführt. In umgekehrter Richtung gibt model_dump() ein dict zurück, während model_dump_json() einen JSON-String zurückgibt.
Wenn du einen Typ validieren musst, der kein Modell ist, z. B. list[OrderItem], verwendest du TypeAdapter:
from pydantic import TypeAdapter
items_adapter = TypeAdapter(list[OrderItem])
items = items_adapter.validate_python([
{"sku": "ABC-0001", "quantity": 2, "unit_price": "5.00"},
{"sku": "XYZ-0002", "quantity": 1, "unit_price": "12.50"},
])
print(len(items), items[1].unit_price)
2 12.50
Du solltest TypeAdapter einmal auf Modulebene erstellen und dann wiederverwenden. Die Initialisierung hat Kosten für das Schema-Aufbau, daher sollte sie nicht bei jedem Request wiederholt werden.
9. Tests mit pytest schreiben
Validierung ist Geschäftslogik und sollte getestet werden. Angenommen, die obigen Modelle befinden sich in der Datei models.py:
from decimal import Decimal
import pytest
from pydantic import ValidationError
from models import Booking, Contact, OrderItem
def test_order_item_coerces_valid_input():
item = OrderItem(sku="ABC-1234", quantity="3", unit_price="19.90")
assert item.quantity == 3
assert item.unit_price == Decimal("19.90")
@pytest.mark.parametrize("quantity", [0, -1, 101])
def test_order_item_rejects_out_of_range_quantity(quantity):
with pytest.raises(ValidationError) as exc_info:
OrderItem(sku="ABC-1234", quantity=quantity, unit_price="10")
error = exc_info.value.errors()[0]
assert error["loc"] == ("quantity",)
assert error["type"] in {"greater_than", "less_than_equal"}
def test_contact_normalizes_phone():
contact = Contact(name="an", phone="+84 912.345.678")
assert contact.phone == "0912345678"
assert contact.name == "An"
def test_booking_rejects_checkout_before_checkin():
with pytest.raises(ValidationError, match="check_out muss nach check_in sein"):
Booking(room_id=1, check_in="2026-10-20", check_out="2026-10-20")
Tests ausführen:
pytest -q
..... [100%]
5 passed
Beachte, dass die Tests auf loc und type assertieren, nicht auf den wörtlichen Inhalt von msg. Dadurch brechen Tests nicht, wenn Pydantic die Fehlermeldung in einer neuen Version ändert. parametrize hilft dir, beide Grenzen 0 und 101 mit nur einer Funktion abzudecken.
10. Häufige Fehler und wann du Pydantic nicht verwenden solltest
API von v1 verwenden. Viele alte Dokumentationen verwenden noch v1-Syntax. Schnellreferenztabelle:
v1 v2
@validator @field_validator
@root_validator @model_validator
class Config: ... model_config = ConfigDict(...)
.dict() / .json() .model_dump() / .model_dump_json()
.parse_obj() / .parse_raw() .model_validate() / .model_validate_json()
Die alte API funktioniert immer noch, gibt aber DeprecationWarning aus. Du solltest zur neuen API migrieren.
Überraschungen mit Lax-Modus. Der Lax-Modus akzeptiert "yes" als True und 1.0 als 1. Bei Finanz- oder ID-Daten kann dieses Verhalten unerwünscht sein. Aktiviere dann strict.
return in Validatoren vergessen. Ein field_validator ohne return setzt das Feld auf None. Ein model_validator(mode="after") ohne return self verursacht auch einen Fehler.
Validierung unbewusst überspringen. model_construct() erstellt eine Instanz ohne Validierung. Diese Methode ist nur für Daten gedacht, denen du absolut vertraust, z. B. Daten aus deiner eigenen Datenbank.
Hinweis: Du solltest Pydantic nicht für alle internen Objekte verwenden. In heißen Schleifen, die Millionen bereits sauberer Objekte verarbeiten, sind die Validierungskosten unnötig. In solchen Fällen sind
dataclassoderNamedTuplebesser geeignet. Verwende Pydantic an der Grenze deines Systems: APIs, Konfigurationsdateien, Nachrichten von anderen Systemen.
11. Fazit
Pydantic v2 verwandelt die Eingabevalidierung von Imperativen in Deklaratives. Du beschreibst Typen mit Type-Hints, fügst Constraints mit Field und Annotated hinzu, schreibst benutzerdefinierte Regeln mit field_validator und model_validator. Globales Verhalten wie das Ablehnen zusätzlicher Felder oder das Deaktivieren der Typkonvertierung wird über ConfigDict gesteuert.
Drei Punkte zum Merken: Verwende model_validate_json für JSON-Eingaben, aktiviere extra="forbid" bei wichtigen Daten, und teste Validierung anhand von loc/type statt des Inhalts der Fehlermeldung. Richtig angewendet, an den Grenzen deines Systems, hilft Pydantic deinem Anwendungskern, nur mit sauberen und korrekten Daten zu arbeiten.