Vai al contenuto principale

Pydantic: una guida con esempi pratici

Scopri cos'è Pydantic, a cosa serve e come si confronta con alternative come Marshmallow o le dataclasses di Python.
· 12 min leggi

Scopri con l'IA

Pydantic è la libreria di convalida dei dati più popolare in Python, capace di trasformare gli hint di tipo in regole di validazione a runtime.

Invece di scrivere decine di if isinstance() e funzioni di validazione personalizzate, definisci una sola volta la tua struttura dati usando la sintassi Python che già conosci.

Pydantic si occupa del resto: valida i dati in ingresso, converte i tipi quando opportuno e fornisce messaggi di errore chiari quando la validazione fallisce.

Considera questa semplice funzione Python con hint di tipo e una corretta documentazione:

def calculate_user_discount(age: int, is_premium_member: bool, purchase_amount: float) -> float:
   """Calculate discount percentage based on user profile and purchase amount."""
   if age >= 65:
       base_discount = 0.15
   elif is_premium_member:
       base_discount = 0.10
   else:
       base_discount = 0.05
  
   return purchase_amount * base_discount

Guardando la sua firma, sai esattamente che tipo di dati accetta e cosa produce dall'altra parte. Ma puoi dire lo stesso dell'utente finale? In fin dei conti, questi hint di tipo servono solo come documentazione — se qualcuno passa un tipo di dato errato, all'interprete di Python non importa:

# Questo gira senza errori, anche se i tipi sono completamente sbagliati!
discount = calculate_user_discount(True, 1, 5)
print(discount)  # Output: 0.5 (True diventa 1, 1 è truthy, quindi 5 * 0.10)

Questo è un classico esempio del typing dinamico di Python. Offre ai programmatori rapidità di sviluppo e libertà, ma a costo di introdurre problemi di validazione dei tipi che spesso emergono in produzione.

In questo tutorial ti spiegherò come usare Pydantic in applicazioni reali. Partiremo dal tuo primo data model e passeremo a pattern avanzati usati in produzione. Scoprirai come validare strutture dati annidate, scrivere validator personalizzati per regole di business complesse e integrare Pydantic con framework popolari come FastAPI e SQLAlchemy.

Qui sotto trovi anche il nostro video tutorial sui fondamenti di Pydantic.

Che cos'è Pydantic?

Quando costruisci applicazioni che interagiscono con l'esterno, è un po' come giocare alla roulette dei dati. Gli utenti potrebbero inserire un numero di telefono come stringa invece che come intero, un'API potrebbe restituire valori null dove ti aspetti numeri, o un file di configurazione potrebbe contenere refusi che mandano in crash la tua applicazione alle 3 di notte.

Gli approcci Python tradizionali a questo problema diventano presto ingestibili. Finisci per scrivere codice di validazione come questo:

def create_user(data):
   # Incubo di validazione manuale
   if not isinstance(data.get('age'), int):
       raise ValueError("Age must be an integer")
   if data['age'] < 0 or data['age'] > 150:
       raise ValueError("Age must be between 0 and 150")
   if not isinstance(data.get('email'), str) or '@' not in data['email']:
       raise ValueError("Invalid email format")
   if not isinstance(data.get('is_active'), bool):
       raise ValueError("is_active must be a boolean")
  
   # Finalmente crea l'utente...
   return User(data['age'], data['email'], data['is_active'])

Moltiplica questo per ogni struttura dati della tua applicazione e passerai più tempo a scrivere validazione che logica di business.

Pydantic risolve questo combinando tre concetti potenti: hint di tipo, validazione a runtime e serializzazione automatica. Invece di controlli manuali, definisci una volta la tua struttura dati usando la sintassi di annotazione dei tipi di Python e Pydantic gestisce automaticamente tutta la validazione:

from pydantic import BaseModel, EmailStr
from typing import Optional

class User(BaseModel):
   age: int
   email: EmailStr
   is_active: bool = True
   nickname: Optional[str] = None

# Pydantic valida e converte i dati automaticamente
user_data = {
   "age": "25",  # La stringa viene convertita in int
   "email": "[email protected]",
   "is_active": "true"  # La stringa viene convertita in bool
}

user = User(**user_data)
print(user.age)  # 25 (come intero)
print(user.model_dump())  # Dizionario pulito in output

Le funzionalità di Pydantic

L'approccio di Pydantic offre diversi vantaggi. La performance è il primo che noterai — il core di validazione è scritto in Rust, risultando più veloce della validazione scritta a mano in Python nella maggior parte dei casi. La tua applicazione può gestire migliaia di richieste senza che la validazione diventi un collo di bottiglia.

Pydantic permette anche l'integrazione con framework moderni. FastAPI, uno dei framework web Python in più rapida crescita, usa modelli Pydantic per generare automaticamente la documentazione delle API, validare i corpi delle richieste e serializzare le risposte. Quando definisci un modello Pydantic, ottieni gratis la generazione dello schema OpenAPI:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class UserCreate(BaseModel):
   name: str
   email: EmailStr
   age: int

@app.post("/users/")
async def create_user(user: UserCreate):
   # FastAPI valida automaticamente il body della richiesta
   # e genera le API docs dal tuo modello Pydantic
   return {"message": f"Created user {user.name}"}

La generazione dello schema JSON avviene automaticamente con ogni modello Pydantic. Questo significa che le tue strutture dati diventano auto-documentate e puoi generare librerie client, regole di validazione per il frontend o schemi di database dalla stessa fonte di verità.

Pydantic vs. Dataclasses vs. Marshmallow

Ma quando scegliere Pydantic rispetto alle alternative? Se stai confrontando Pydantic e dataclasses, la decisione dipende dalle esigenze di validazione. Il @dataclass di Python è perfetto per semplici contenitori di dati quando ti fidi dell'input, mentre Pydantic eccelle quando ti servono validazione, serializzazione e integrazione con i framework web:

from dataclasses import dataclass
from pydantic import BaseModel

# Dataclass: veloce, semplice, nessuna validazione
@dataclass
class UserDataclass:
   name: str
   age: int

# Pydantic: validazione, serializzazione, integrazione con framework
class UserPydantic(BaseModel):
   name: str
   age: int

Confrontando Pydantic con Marshmallow, l'approccio basato su hint di tipo di Pydantic risulta più naturale per gli sviluppatori Python, mentre Marshmallow usa classi schema. Pydantic offre anche prestazioni migliori e un'integrazione più semplice con i moderni framework asincroni.

Pydantic dà il meglio quando costruisci API, elabori dati esterni, gestisci configurazioni o in qualsiasi scenario in cui i fallimenti di validazione dovrebbero essere intercettati subito, invece di causare bug misteriosi più avanti. Converte gli errori a runtime in messaggi di validazione chiari e azionabili che aiutano sia gli sviluppatori sia gli utenti a capire cosa è andato storto.

Primi passi con Pydantic

Vediamo come usare Pydantic nei tuoi progetti, dall'installazione alla creazione dei primi data model.

Installazione e setup

Configurare Pydantic è semplice, ma alcune trappole comuni possono mettere in difficoltà chi è alle prime armi. Ti guiderò nel processo corretto di installazione aiutandoti a evitare gli errori che portano a debug inutili.

Per prima cosa, crea un ambiente virtuale per isolare le dipendenze del progetto. Questo evita conflitti di versione che possono causare strani errori di import in seguito:

# Crea e attiva un ambiente virtuale (funziona in zsh e bash)
python -m venv pydantic_env
source pydantic_env/bin/activate  # Su macOS/Linux
# pydantic_env\Scripts\activate  # Su Windows

Ora installa Pydantic con il seguente comando:

pip install pydantic

Per la funzionalità di validazione delle email, di cui probabilmente avrai bisogno in applicazioni reali (e in questo tutorial), installa gli extra per le email:

pip install "pydantic[email]"

Le virgolette attorno a pydantic[email] sono importanti in quasi tutti i terminali. Senza di esse, potresti vedere l'errore "no matches found".

Nota: se incontri ModuleNotFoundError: No module named 'pydantic', verifica questi punti:

  1. Ambiente Python errato: assicurati che il tuo ambiente virtuale sia attivo
  2. Più versioni di Python: verifica di usare la stessa versione di Python con cui hai installato Pydantic
  3. Configurazione dell'IDE: il tuo editor potrebbe usare un interprete Python diverso

Avvertenza critica sui nomi: non chiamare mai il tuo file Python pydantic.py. Creeresti un import circolare che romperà il codice con messaggi d'errore fuorvianti. Python proverà a importare il tuo file invece della libreria Pydantic reale.

Il tuo primo modello Pydantic

Costruiamo un semplice modello utente per vedere Pydantic in azione. Invece di partire da esempi astratti, risolviamo un problema reale: validare i dati di registrazione utente da un form web.

from pydantic import BaseModel, EmailStr
from typing import Optional
from datetime import datetime

class User(BaseModel):
   name: str
   email: EmailStr
   age: int
   is_active: bool = True
   created_at: datetime = None

# Test con dati puliti
aean_data = {
   "name": "Alice Johnson",
   "email": "[email protected]",
   "age": 28
}

user = User(**clean_data)
print(f"User created: {user.name}, Age: {user.age}")
print(f"Model output: {user.model_dump()}")

Output:

User created: Alice Johnson, Age: 28
Model output: {'name': 'Alice Johnson', 'email': '[email protected]', 'age': 28, 'is_active': True, 'created_at': None}

Vediamo nel dettaglio cosa succede in questa definizione di modello:

  • Creazione di un modello Pydantic: la tua classe User eredita da BaseModel, ottenendo tutte le capacità di validazione e serializzazione di Pydantic. Questa ereditarietà trasforma una normale classe Python in uno strumento di validazione dei dati.
  • Definizioni dei campi: ogni riga nella classe definisce un campo con il tipo atteso. La sintassi name: str dice a Pydantic che name deve essere una stringa, age: int significa che age deve essere un intero, e così via.
  • EmailStr spiegato: EmailStr è un tipo speciale di Pydantic che valida automaticamente gli indirizzi email. Arriva dal pacchetto pydantic[email] che hai installato prima e usa espressioni regolari per assicurare che il formato dell'email sia valido. Se qualcuno passa "not-an-email", Pydantic solleverà un errore di validazione.
  • Valori predefiniti: campi come is_active: bool = True hanno valori di default. Se non fornisci questi campi quando crei un utente, Pydantic userà i default. Il = None per created_at rende questo campo opzionale.
  • Istanziazione del modello: quando chiami User(**clean_data), ** spacchetta il dizionario e passa ogni coppia chiave-valore come argomento nominato al costruttore del modello.

Ora vediamo in azione la conversione automatica dei tipi di Pydantic:

# Dati disordinati che funzionano comunque
messy_data = {
   "name": "Bob Smith",
   "email": "[email protected]",
   "age": "35",  # Stringa invece di int
   "is_active": "true"  # Stringa invece di bool
}

user = User(**messy_data)
print(f"Age type: {type(user.age)}")  # <class 'int'>
print(f"Is active type: {type(user.is_active)}")  # <class 'bool'>

Ora vediamo in azione la conversione automatica dei tipi di Pydantic:

# Dati disordinati che funzionano comunque
messy_data = {
   "name": "Bob Smith",
   "email": "[email protected]",
   "age": "35",  # Stringa invece di int
   "is_active": "true"  # Stringa invece di bool
}

user = User(**messy_data)
print(f"Age type: {type(user.age)}")  # <class 'int'>
print(f"Is active type: {type(user.is_active)}")  # <class 'bool'>

Output:

Age type: <class 'int'>

Is active type: <class 'bool'>

Come puoi vedere, i campi age e is_inactive vengono convertiti automaticamente nei formati corretti durante la validazione.

Quando la validazione fallisce, Pydantic fornisce messaggi di errore chiari:

from pydantic import ValidationError

try:
   invalid_user = User(
       name="",  # Stringa vuota
       email="not-an-email",  # Email non valida
       age=-5  # Età negativa
   )
except ValidationError as e:
   print(e)

Output:

   # Mostra esattamente quali campi sono falliti e perché
1 validation error for User
email
 value is not a valid email address: An email address must have an @-sign. [type=value_error, input_value='not-an-email', input_type=str]

BaseModel vs. data classes

Capire quando usare il BaseModel di Pydantic rispetto a @dataclass di Python ti aiuta a scegliere lo strumento giusto per ogni situazione.

Le dataclasses Python sono perfette per semplici contenitori di dati quando controlli l'input:

from dataclasses import dataclass

@dataclass
class ProductDataclass:
   name: str
   price: float
   in_stock: bool

# Veloce, semplice, ma nessuna validazione
product = ProductDataclass("Laptop", 999.99, True)

# Funziona anche questo, anche se i tipi sono sbagliati:
broken_product = ProductDataclass(123, "expensive", "maybe")

I modelli Pydantic aggiungono validazione, serializzazione e integrazione con i framework:

from pydantic import BaseModel, Field

class ProductPydantic(BaseModel):
   name: str = Field(min_length=1)
   price: float = Field(gt=0)  # Deve essere maggiore di 0
   in_stock: bool

# La validazione automatica impedisce dati errati
try:
   product = ProductPydantic(name="", price=-10, in_stock="maybe")
except ValidationError as e:
   print("La validazione ha intercettato gli errori!")

# I dati validi funzionano perfettamente
good_product = ProductPydantic(
   name="Laptop",
   price="999.99",  # La stringa viene convertita in float
   in_stock=True
)

Quando scegliere ciascun approccio:

  • Usa le dataclasses per strutture dati interne, oggetti di configurazione o quando la performance è critica e ti fidi delle fonti dati
  • Usa Pydantic per endpoint API, input utente, parsing di dati esterni o quando ti serve la serializzazione JSON

Pydantic aggiunge un po' di overhead rispetto alle dataclasses, ma questo costo è di solito trascurabile rispetto ai bug che previene e al tempo di sviluppo che fa risparmiare. Per applicazioni web, l'integrazione automatica con framework come FastAPI rende Pydantic la scelta più ovvia.

Le funzionalità di validazione e serializzazione diventano più preziose man mano che l'applicazione cresce. Partire con modelli Pydantic ti dà fondamenta solide che scalano con le tue esigenze.

Costruire data model con Pydantic

Ora che conosci le basi, affrontiamo le sfide che incontrerai costruendo applicazioni pronte per la produzione.

Validazione dei campi e vincoli

Considera un'API di catalogo prodotti in cui i prezzi arrivano da più fornitori con standard di formattazione diversi. Alcuni inviano i prezzi come stringhe, altri come float e, ogni tanto, qualcuno invia un prezzo negativo che manda in crash il tuo sistema di fatturazione.

La funzione Field() di Pydantic trasforma gli hint di tipo di base in regole di validazione sofisticate che proteggono la tua applicazione:

from pydantic import BaseModel, Field
from decimal import Decimal
from typing import Optional

class Product(BaseModel):
   name: str = Field(min_length=1, max_length=100)
   price: Decimal = Field(gt=0, le=10000)  # Maggiore di 0, minore o uguale a 10.000
   description: Optional[str] = Field(None, max_length=500)
   category: str = Field(..., pattern=r'^[A-Za-z\s]+$')  # Solo lettere e spazi
   stock_quantity: int = Field(ge=0)  # Maggiore o uguale a 0
   is_available: bool = True

# Funziona: tutti i vincoli soddisfatti
valid_product = Product(
   name="Wireless Headphones",
   price="199.99",  # La stringa viene convertita in Decimal
   description="High-quality wireless headphones",
   category="Electronics",
   stock_quantity=50
)

# Fallisce con messaggi di errore chiari
try:
   invalid_product = Product(
       name="",  # Troppo corto
       price=-50,  # Prezzo negativo
       category="Electronics123",  # Contiene numeri
       stock_quantity=-5  # Stock negativo
   )
except ValidationError as e:
   print(f"Validation errors: {len(e.errors())} issues found")

Ogni parametro di Field() ha uno scopo specifico: min_length e max_length prevengono violazioni dello schema del database, gt e le creano confini di logica di business e pattern valida i dati formattati usando espressioni regolari. La sintassi Field(...) con i puntini di sospensione indica i campi obbligatori, mentre Field(None, ...) crea campi opzionali con regole di validazione.

Coercizione dei tipi vs validazione rigorosa

Per impostazione predefinita, Pydantic converte i tipi compatibili invece di rifiutarli a priori. Questa flessibilità funziona bene con input utente, ma alcuni scenari richiedono una corrispondenza esatta dei tipi:

from pydantic import BaseModel, Field, ValidationError

# Default: coercizione dei tipi permissiva
class FlexibleOrder(BaseModel):
   order_id: int
   total_amount: float
   is_paid: bool

# Questi funzionano tutti grazie alla conversione automatica
flexible_order = FlexibleOrder(
   order_id="12345",  # Stringa in int
   total_amount="99.99",  # Stringa in float
   is_paid="true"  # Stringa in bool
)

# Validazione rigorosa quando serve precisione
class StrictOrder(BaseModel):
   model_config = {"str_strip_whitespace": True, "validate_assignment": True}
  
   order_id: int = Field(strict=True)
   total_amount: float = Field(strict=True)
   is_paid: bool = Field(strict=True)

Il dizionario model_config controlla il comportamento della validazione per tutto il modello. L'opzione str_strip_whitespace pulisce automaticamente gli input stringa, mentre validate_assignment assicura che le modifiche ai campi dopo la creazione del modello attivino comunque la validazione. I singoli campi possono sovrascrivere queste impostazioni con Field(strict=True) in situazioni che richiedono corrispondenza esatta dei tipi, come calcoli finanziari o dati scientifici.

Modelli annidati e dati complessi

Le applicazioni reali gestiscono strutture dati complesse e interconnesse. Un ordine e-commerce contiene informazioni sul cliente, indirizzi di spedizione e più articoli prodotto, ciascuno con la propria validazione:

from typing import List
from datetime import datetime

class Address(BaseModel):
   street: str = Field(min_length=5)
   city: str = Field(min_length=2)
   postal_code: str = Field(pattern=r'^\d{5}(-\d{4})?$')
   country: str = "USA"

class Customer(BaseModel):
   name: str = Field(min_length=1)
   email: EmailStr
   shipping_address: Address
   billing_address: Optional[Address] = None

class OrderItem(BaseModel):
   product_id: int = Field(gt=0)
   quantity: int = Field(gt=0, le=100)
   unit_price: Decimal = Field(gt=0)

class Order(BaseModel):
   order_id: str = Field(pattern=r'^ORD-\d{6}$')
   customer: Customer
   items: List[OrderItem] = Field(min_items=1)
   order_date: datetime = Field(default_factory=datetime.now)

# Validazione complessa di dati annidati
order_data = {
   "order_id": "ORD-123456",
   "customer": {
       "name": "John Doe",
       "email": "[email protected]",
       "shipping_address": {
           "street": "123 Main Street",
           "city": "Anytown",
           "postal_code": "12345"
       }
   },
   "items": [
       {"product_id": 1, "quantity": 2, "unit_price": "29.99"},
       {"product_id": 2, "quantity": 1, "unit_price": "149.99"}
   ]
}

order = Order(**order_data)
print(f"Order validated with {len(order.items)} items")

Pydantic valida ricorsivamente le strutture annidate — il campo Customer diventa un oggetto Customer a tutti gli effetti, che valida a sua volta il proprio campo Address. La sintassi List[OrderItem] valida ogni elemento della lista come OrderItem, mentre Field(min_items=1) impedisce che ordini vuoti raggiungano il tuo sistema di inventario. Usare default_factory=datetime.now crea timestamp unici per ogni istanza di ordine.

Campi opzionali e gestione di None

Operazioni diverse richiedono dati diversi. La creazione utente pretende informazioni complete, mentre gli aggiornamenti dovrebbero accettare modifiche parziali:

from typing import Optional

class UserCreate(BaseModel):
   name: str = Field(min_length=1)
   email: EmailStr
   age: int = Field(ge=13, le=120)
   phone: Optional[str] = Field(None, pattern=r'^\+?1?\d{9,15}$')

class UserUpdate(BaseModel):
   name: Optional[str] = Field(None, min_length=1)
   email: Optional[EmailStr] = None
   age: Optional[int] = Field(None, ge=13, le=120)
   phone: Optional[str] = Field(None, pattern=r'^\+?1?\d{9,15}$')

# Richiesta PATCH con dati parziali
update_data = {"name": "Jane Smith", "age": 30}
user_update = UserUpdate(**update_data)

# Serializza solo i campi forniti
patch_data = user_update.model_dump(exclude_none=True)
print(f"Fields to update: {list(patch_data.keys())}")

La serializzazione converte gli oggetti Pydantic in dizionari o stringhe JSON per l'archiviazione o la trasmissione. Il metodo model_dump() gestisce questa conversione, con exclude_none=True che rimuove i campi non forniti. Questo pattern è perfetto per le richieste PATCH in cui i client inviano solo i campi da modificare, prevenendo sovrascritture accidentali nel database.

Queste basi ti preparano alla prossima sfida: implementare logiche di validazione personalizzate che catturino le regole di business specifiche della tua applicazione.

Validazione personalizzata e integrazione nel mondo reale

Costruire applicazioni di produzione significa gestire dati che non rientrano nei pattern standard di controllo dei tipi.

Pensa a un form di registrazione utente in cui i requisiti della password variano in base al piano di abbonamento, o a un'API che riceve indirizzi da più paesi con formati di CAP diversi. Questi scenari richiedono logiche di validazione personalizzate che catturino le tue regole di business specifiche, integrandosi senza attriti con framework web e sistemi di configurazione.

Questa sezione ti mostra come implementare pattern pratici di validazione personalizzata, integrare i modelli Pydantic con FastAPI per la documentazione automatica delle API e gestire le impostazioni dell'app tramite variabili d'ambiente usando i file .env, approccio su cui si basano la maggior parte delle applicazioni in produzione.

Validator di campo e validazione del modello

Quando è la logica di business a determinare la validità dei dati, il decorator @field_validator di Pydantic trasforma le tue funzioni di validazione in parte integrante del modello. Considera un sistema di registrazione utente in cui livelli di abbonamento diversi hanno requisiti di password differenti:

from pydantic import BaseModel, field_validator, Field
import re

class UserRegistration(BaseModel):
   username: str = Field(min_length=3)
   email: EmailStr
   password: str
   subscription_tier: str = Field(pattern=r'^(free|pro|enterprise)$')
  
   @field_validator('password')
   @classmethod
   def validate_password_complexity(cls, password, info):
       tier = info.data.get('subscription_tier', 'free')
      
       if len(password) < 8:
           raise ValueError('Password must be at least 8 characters')
          
       if tier == 'enterprise' and not re.search(r'[A-Z]', password):
           raise ValueError('Enterprise accounts require uppercase letters')
          
       return password

Il decorator @field_validator ti dà accesso agli altri valori dei campi tramite il parametro info.data, permettendo regole di validazione che dipendono da più campi. Il validator gira dopo che il controllo di tipo di base è passato, quindi puoi assumere in sicurezza che subscription_tier sia uno dei valori ammessi.

Per validazioni che coinvolgono più campi, il decorator @model_validator viene eseguito dopo che tutti i singoli campi sono stati validati:

from datetime import datetime
from pydantic import model_validator

class EventRegistration(BaseModel):
   start_date: datetime
   end_date: datetime
   max_attendees: int = Field(gt=0)
   current_attendees: int = Field(ge=0)
  
   @model_validator(mode='after')
   def validate_event_constraints(self):
       if self.end_date <= self.start_date:
           raise ValueError('Event end date must be after start date')
          
       if self.current_attendees > self.max_attendees:
           raise ValueError('Current attendees cannot exceed maximum')
          
       return self

Il parametro mode='after' assicura che il validator riceva un'istanza del modello completamente costruita, rendendolo perfetto per logiche di business che richiedono accesso a tutti i campi validati. Il validator deve restituire self per indicare che la validazione è andata a buon fine.

Integrazione con FastAPI

L'integrazione tra FastAPI e Pydantic crea validazione automatica delle richieste e documentazione delle API. Il pattern chiave consiste nel creare modelli separati per le diverse operazioni, dandoti controllo su quali dati fluiscono in ogni direzione:

from fastapi import FastAPI
from typing import Optional
from datetime import datetime

app = FastAPI()

class UserCreate(BaseModel):
   username: str = Field(min_length=3)
   email: EmailStr
   password: str = Field(min_length=8)

class UserResponse(BaseModel):
   id: int
   username: str
   email: EmailStr
   created_at: datetime
  
@app.post("/users/", response_model=UserResponse)
async def create_user(user: UserCreate):
   # FastAPI valida automaticamente il body della richiesta
   new_user = {
       "id": 1,
       "username": user.username,
       "email": user.email,
       "created_at": datetime.now()
   }
   return UserResponse(**new_user)

La separazione tra modelli di input e di output offre diversi benefici. I modelli di input possono includere regole di validazione e campi obbligatori, mentre i modelli di output controllano esattamente quali dati vengono inviati ai client. FastAPI genera automaticamente la documentazione OpenAPI dai tuoi modelli Pydantic, creando API docs interattive che gli sviluppatori possono usare per testare gli endpoint.

Per le operazioni di aggiornamento, puoi creare modelli in cui tutti i campi sono opzionali:

class UserUpdate(BaseModel):
   username: Optional[str] = Field(None, min_length=3)
   email: Optional[EmailStr] = None

@app.patch("/users/{user_id}")
async def update_user(user_id: int, user_update: UserUpdate):
   # Aggiorna solo i campi forniti
   update_data = user_update.model_dump(exclude_unset=True)
   # Qui la logica di aggiornamento del database
   return {"message": f"Updated user {user_id}"}

Il parametro exclude_unset=True nelle operazioni PATCH assicura che vengano aggiornati solo i campi esplicitamente forniti, prevenendo sovrascritture accidentali. Questo pattern è perfetto per le REST API in cui i client inviano aggiornamenti parziali.

Gestione della configurazione con variabili d'ambiente

Le applicazioni di produzione hanno bisogno di una gestione della configurazione sicura e adatta al deploy. BaseSettings di Pydantic, combinata con i file .env, fornisce configurazioni tipizzate che funzionano in ambienti di sviluppo, staging e produzione.

Per prima cosa, crea un file .env nella root del progetto:

# File .env
DATABASE_URL=postgresql://user:password@localhost:5432/myapp
SECRET_KEY=your-secret-key-here
DEBUG=false
ALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com

Quindi definisci il tuo modello di impostazioni:

from pydantic import BaseSettings, Field
from typing import List

class AppSettings(BaseSettings):
   database_url: str = Field(description="Database connection URL")
   secret_key: str = Field(description="Secret key for JWT tokens")
   debug: bool = Field(default=False)
   allowed_hosts: List[str] = Field(default=["localhost"])
  
   class Config:
       env_file = ".env"
       case_sensitive = False

# Carica automaticamente le impostazioni da environment e file .env
settings = AppSettings()

La classe BaseSettings legge automaticamente da variabili d'ambiente, file .env e argomenti da riga di comando. Le variabili d'ambiente hanno precedenza sui valori del file .env, rendendo semplice sovrascrivere le impostazioni nei diversi ambienti di deploy. L'impostazione case_sensitive = False consente flessibilità nella denominazione delle variabili d'ambiente.

Per applicazioni complesse, puoi organizzare le impostazioni in gruppi logici:

class DatabaseSettings(BaseSettings):
   url: str = Field(env="DATABASE_URL")
   max_connections: int = Field(default=5, env="DB_MAX_CONNECTIONS")

class AppSettings(BaseSettings):
   secret_key: str
   debug: bool = False
   database: DatabaseSettings = DatabaseSettings()
  
   class Config:
       env_file = ".env"

settings = AppSettings()
# Accedi alla configurazione annidata
db_url = settings.database.url

Questo approccio annidato mantiene insieme le impostazioni correlate pur mantenendo una chiara separazione tra i vari componenti dell'applicazione. Ogni gruppo di impostazioni può avere il proprio prefisso di variabili d'ambiente e regole di validazione.

L'approccio con file .env funziona con piattaforme di deploy come Heroku, AWS e Docker, dove le variabili d'ambiente sono lo standard per configurare le applicazioni. La tua applicazione ottiene tipizzazione e validazione pur seguendo i pattern cloud-native di configurazione che i team di operations si aspettano.

Questi pattern costituiscono la base per costruire applicazioni manutenibili che gestiscono complessità reali. Il sistema di validazione di Pydantic si adatta ai tuoi requisiti specifici fornendo messaggi di errore chiari e documentazione automatica che aiuta tutto il team a comprendere le strutture dati attese dall'applicazione.

Conclusione

Hai visto come Pydantic può evitarti il lavoro noioso di scrivere a mano il codice di validazione. Invece di riempire le funzioni di controlli isinstance() e gestione personalizzata degli errori, puoi definire una volta la tua struttura dati e lasciare che Pydantic faccia il resto.

Assicurati di leggere la sezione FAQ qui sotto per domande più specifiche su Pydantic. Per documentazione completa e pattern avanzati, ecco alcune risorse utili:

FAQs

La validazione di Pydantic rallenta la mia applicazione?

Pydantic V2 è costruito in Rust sotto il cofano, ed è molto veloce. Per la maggior parte delle applicazioni, l'overhead della validazione è minimo rispetto ai bug che previene. Puoi usare model_construct() per saltare la validazione per fonti dati affidabili quando la performance è critica.

Quando dovrei usare Pydantic invece delle dataclasses di Python?

Usa Pydantic quando ti serve la validazione, lavori con fonti dati esterne o costruisci API. Usa le dataclasses per semplici strutture dati interne quando ti fidi dell'input. Pydantic eccelle quando i dati arrivano da utenti, API o file di configurazione.

Come gestisco gli errori di validazione Pydantic nelle applicazioni in produzione?

Intercetta le eccezioni ValidationError ed estrai la lista .errors() per ottenere informazioni di errore strutturate. Ogni errore contiene i campi loc, msg e type che ti aiutano a fornire messaggi d'errore comprensibili agli utenti o a registrare dettagli utili per il debug.

Posso aggiungere Pydantic al codice esistente senza rompere tutto?

Sì! Inizia incapsulando i punti d'ingresso dei dati esterni (richieste API, parsing di file) con modelli Pydantic. Puoi convertire gradualmente le strutture dati interne. Pydantic funziona insieme al codice esistente e non richiede di cambiare subito l'intera codebase.

Qual è l'errore più comune quando si inizia con Pydantic?

Non comprendere la coercizione dei tipi. Per default, Pydantic converte i tipi compatibili (ad esempio la stringa "123" in intero 123). Se ti serve un controllo rigoroso dei tipi, usa Field(strict=True) o imposta la modalità strict nella configurazione del modello per impedire conversioni automatiche.

Bexruz (Bex) Tuychiev

Autore

Bexruz (Bex) Tuychiev

Sono un creator di contenuti sulla data science con oltre 2 anni di esperienza e uno dei più seguiti su Medium. Mi piace scrivere articoli dettagliati su AI e ML con un pizzico di sarcasmo, perché bisogna pur fare qualcosa per renderli un po’ meno noiosi. Ho pubblicato oltre 130 articoli e un corso su DataCamp, con un altro in arrivo. I miei contenuti sono stati visti da oltre 5 milioni di persone, 20.000 delle quali sono diventate follower sia su Medium che su LinkedIn. 

Argomenti

Correlato