Python - Pydantic

Pydantic valida i serialitza de manera automàtica les dades JSON que consumeixes o produeixes.

Introducció

Pydantic és una biblioteca de validació de dades que utilitza Python - Typing

Instal·la la biblioteca pydantic:

uv add pydantic

Models

Un model és una classe que hereta de BaseModel i anota amb tipus els atributs de la classe.

Són molt semblants a un @dataclass, excepte que estan pensants per:

  1. La validació i serialització de dades JSON
  2. La generació d’esquemes JSON.

Per serialitzar dades, Pydantic utilitzar una llibreria escrita en Rust: jiter

A continuació tens un exemple d’una classe User que hereta de BaseModel i defineix camps com a atributs anotats:

from pydantic import BaseModel
class User(BaseModel):
id: int
name: str | None = None

Llavors el model es pot instanciar:

user: User = User(id=1, name="David")

La inicialització de l’objecte fa tota l’anàlisi i validació.

Si no s’aixeca cap excepció ValidationError, saps que la instància del model resultant és vàlida:

assert user.id == 1
assert user.name == "David"

Però si escrius aquest codi, Python - PyCharm et dirà que és erroni:

from pydantic import BaseModel
class User(BaseModel):
id: int
name: str | None = None
david: User = User(name="David")

I pydantic genera un error en temps d’execució

Terminal window
> python test.py
...
pydantic_core._pydantic_core.ValidationError: 1 validation error for User
id
Field required [type=missing, input_value={'name': 'David'}, input_type=dict]

S’aixecarà una única excepció independentment del nombre d’errors trobats, i aquell error de validació contindrà informació sobre tots els errors i com es van produir.

Per defecte, els models són mutables i els valors dels camps es poden canviar mitjançant l’assignació d’atributs:

user.id = 321
assert user.id == 321

Validant dades

Pydantic utilitza un dict per guardar les dades: podem passar directament un “punter” a un dict per crear un User.

Si crees objectes a partir de dades de sistemes externs, no hi ha cap garantia que siguin correctes:

from pydantic import BaseModel
from typing import Any
class User(BaseModel):
id: int
name: str | None = None
data: Any = {"id": 1, "name": "David"}
User(**data)
data = {"name": "apple", "price": 3}
User(**data) # Error de validació

Pydantic proporciona tres mètodes a les classes de model per analitzar dades:

model_validate()

Això és molt similar al mètode __init__ del model, excepte que pren un diccionari o un objecte en lloc d’arguments amb paraula clau.

Si l’objecte passat no es pot validar, o si no és un diccionari o una instància del model en qüestió, s’aixecarà un ValidationError.

from datetime import datetime
from pydantic import BaseModel, ValidationError
class User(BaseModel):
id: int
name: str = 'John Doe'
signup_ts: datetime | None = None
user = User.model_validate({'id': 123, 'name': 'James'})
print(user)
try:
User.model_validate(['not', 'a', 'dict'])
except ValidationError as e:
print(e)
1 validation error for User
Input should be a valid dictionary or instance of User [type=model_type, input_value=['not', 'a', 'dict'], input_type=list]
For further information visit https://errors.pydantic.dev/2.11/v/model_type

model_validate_json()

Això valida les dades proporcionades com un string JSON o objecte bytes. Si les teves dades entrants són una càrrega útil JSON, generalment es considera més ràpid (en lloc d’analitzar manualment les dades com un diccionari).

user = User.model_validate_json('{"id": 123, "name": "James"}')
print(user)
id=123 name='James' signup_ts=None
try:
user = User.model_validate_json('{"id": 123, "name": 123}')
except ValidationError as e:
print(e)
1 validation error for User
name
Input should be a valid string [type=string_type, input_value=123, input_type=int]
For further information visit https://errors.pydantic.dev/2.11/v/string_type
try:
user = User.model_validate_json('invalid JSON')
except ValidationError as e:
print(e)
1 validation error for User
Invalid JSON: expected value at line 1 column 1 [type=json_invalid, input_value='invalid JSON', input_type=str]
For further information visit https://errors.pydantic.dev/2.11/v/json_invalid

model_validate_strings()

Ppren un diccionari (pot estar niuat) amb claus i valors de tipus string i valida les dades en mode JSON perquè aquestes strings puguin ser convertides als tipus correctes.

user = User.model_validate_strings({'id': '123', 'name': 'James'})
print(user)
#> id=123 name='James' signup_ts=None
user = User.model_validate_strings(
{'id': '123', 'name': 'James', 'signup_ts': '2024-04-01T12:00:00'}
)
print(user)
#> id=123 name='James' signup_ts=datetime.datetime(2024, 4, 1, 12, 0)
try:
user = User.model_validate_strings(
{'id': '123', 'name': 'James', 'signup_ts': '2024-04-01'}, strict=True
)
except ValidationError as e:
print(e)
"""
1 validation error for User
signup_ts
Input should be a valid datetime, invalid datetime separator, expected `T`, `t`, `_` or space [type=datetime_parsing, input_value='2024-04-01', input_type=str]
"""

Serialització

La instància del model es pot serialitzar utilitzant el mètode model_dump:

assert user.model_dump() == {'id': 1, 'name': 'David'}

El mètode .model_dump_json() serialitza un model directament a un string codificat en JSON que és equivalent al resultat produït per .model_dump().

from datetime import datetime
from pydantic import BaseModel
class BarModel(BaseModel):
whatever: int
class FooBarModel(BaseModel):
foo: datetime
bar: BarModel
m = FooBarModel(foo=datetime(2032, 6, 1, 12, 13, 14), bar={'whatever': 123})
print(m.model_dump_json())
#> {"foo":"2032-06-01T12:13:14","bar":{"whatever":123}}
print(m.model_dump_json(indent=2))
"""
{
"foo": "2032-06-01T12:13:14",
"bar": {
"whatever": 123
}
}
"""

Models niuats

Un model pot utilitzar altres models.

Si tens aquest diagrama:

Order

id: int

Client

id: int

name: str

Pots escriure aquest codi:

from pydantic import BaseModel
class Client(BaseModel):
id: int
name: str
class Order(BaseModel):
id: int
client: Client
data = {"id": 1, "client": {"id": 45, "name": "David"}}
order: Order = Order.model_validate(data)
assert order.client.id == 45

Activitat

Genera les classes corresponents a aquest diagrama:

1.**

Order

id: int

Client

id: int

name: str

Product

id: int

name: str

price: float

OrderItem

quantity: int

Crea un objecte Order a partir d’un dict:

Field

La funció Field s’utilitza per personalitzar i afegir metadades als camps dels models.

Restriccions numèriques

Hi ha alguns arguments amb paraula clau que es poden utilitzar per restringir valors numèrics:

  • gt - major que
  • lt - menor que
  • ge - major o igual que
  • le - menor o igual que
  • multiple_of - un múltiple del nombre donat
  • allow_inf_nan - permet valors 'inf', '-inf', 'nan'

Aquí tens un exemple:

from pydantic import BaseModel, Field
class Foo(BaseModel):
positive: int = Field(gt=0)
non_negative: int = Field(ge=0)
negative: int = Field(lt=0)
non_positive: int = Field(le=0)
even: int = Field(multiple_of=2)
love_for_pydantic: float = Field(allow_inf_nan=True)
foo = Foo(
positive=1,
non_negative=0,
negative=-1,
non_positive=0,
even=2,
love_for_pydantic=float('inf'),
)
print(foo)
positive=1 non_negative=0 negative=-1 non_positive=0 even=2 love_for_pydantic=inf

Restriccions de strings

Hi ha camps que es poden utilitzar per restringir strings:

  • min_length: Longitud mínima de la cadena.
  • max_length: Longitud màxima de la cadena.
  • pattern: Una expressió regular que la cadena ha de complir.

Aquí tens un exemple:

from pydantic import BaseModel, Field
class Foo(BaseModel):
short: str = Field(min_length=3)
long: str = Field(max_length=10)
regex: str = Field(pattern=r'^\d*$')
foo = Foo(short='foo', long='foobarbaz', regex='123')
print(foo)
short='foo' long='foobarbaz' regex='123'

Immutabilitat

El paràmetre frozen s’utilitza per emular el comportament de dataclass congelat. S’utilitza per evitar que el camp rebi un valor nou després que el model es crea (immutabilitat).

from pydantic import BaseModel, Field, ValidationError
class User(BaseModel):
name: str = Field(frozen=True)
age: int
user = User(name='John', age=42)
try:
user.name = 'Jane'
except ValidationError as e:
print(e)
"""
1 validation error for User
name
Field is frozen [type=frozen_field, input_value='Jane', input_type=str]
"""

Més informació a Concepts - Fields

JSON

Parsing

Amb pydantic pots consumir dades JSON.

En aquest exemple, demanes que la validació sigui estricta:

Pydantic proporciona anàlisi JSON integrat, que ajuda a aconseguir:

from datetime import date
from typing import Tuple
from pydantic import BaseModel, ConfigDict, ValidationError
class Event(BaseModel):
model_config = ConfigDict(strict=True)
when: date
where: Tuple[int, int]
data: str = '{"when": "1987-01-28", "where": [51, -1]}'
event: Event = Event.model_validate_json(data)
assert event.where[0] == 51

Concepts - JSON

TODO