Python e la validazione fantasma
/ 3 min read
Updated:Table of Contents
La scorsa settimana mi è capitato di mettere mano a un progetto Python legacy per fare un paio di modifiche.
Indovina qual era la causa della maggior parte dei bug? La mancanza di una validazione dei dati centralizzata e robusta. L’applicativo si rompeva continuamente a causa di vincoli non gestiti a monte ma imposti da un sistema esterno con cui il nostro software deve comunicare. Nello specifico, questo sistema esterno:
- Rifiuta stringhe più corte di 2 caratteri.
- Va in errore se il nome o il cognome contengono numeri o caratteri speciali.
Per ripulire il codice ed evitare controlli manuali infiniti (i classici if len(nome) < 2), ho deciso di introdurre Pydantic. Sfruttando i type hints nativi di Python, Pydantic permette di validare i dati in modo elegante e dichiarativo.
Vediamo un primo approccio semplice:
from pydantic import BaseModel
class Persona(BaseModel): nome: str cognome: str sesso: str
# Uso baseperson_mario = Persona( nome='Mario', cognome='Rossi', sesso='M')Questo modello funziona, ma è troppo permissivo. Lascia passare anomalie che inquinerebbero i nostri dati o farebbero fallire l’integrazione successiva, come queste:
# Spazi vuoti extra, numeri nel cognome o valori non standard per il sessoperson_1 = Persona( nome='Mario ', cognome='Rossi', sesso='M')person_2 = Persona( nome='Mario', cognome='Rossi 13', sesso='M')person_3 = Persona( nome='Mario', cognome='Rossi', sesso='Maschio')Come possiamo migliorarlo? Saliamo di livello usando Field:
from pydantic import BaseModel, Field
class PersonaV2(BaseModel): nome: str = Field( min_length=2, max_length=50, strip_whitespace=True ) cognome: str = Field( min_length=2, max_length=50, strip_whitespace=True ) sesso: str = Field( min_length=1, max_length=1, strip_whitespace=True )In questo modo sistemiamo la lunghezza minima e “puliamo” gli spazi superflui all’inizio e alla fine delle stringhe. Rimane però un problema: il sesso accetta ancora qualsiasi lettera (es. “X”) e i campi nome/cognome accettano comunque numeri e caratteri speciali.
Per blindare definitivamente il modello, possiamo sfruttare la potenza di Annotated (introdotto in Python 3.9) e StringConstraints di Pydantic v2 per creare dei tipi personalizzati e riutilizzabili:
from enum import Enumfrom typing import Annotatedfrom pydantic import BaseModel, BeforeValidator, StringConstraints
class PersonaSessoEnum(str, Enum): M = "M" F = "F"
# Tipo personalizzato per i nomi validiValidName = Annotated[ str, StringConstraints( strip_whitespace=True, min_length=2, max_length=50, pattern=r"^[a-zA-Z'\s]+$" ),]
# Validatore preventivo per normalizzare l'input del sessoValidSex = Annotated[ PersonaSessoEnum, BeforeValidator(lambda v: v.upper().strip() if isinstance(v, str) else v),]
class PersonaDefinitiva(BaseModel): nome: ValidName cognome: ValidName sesso: ValidSexGrazie a questa struttura, nome e cognome accetteranno solo lettere dell’alfabeto (senza accenti) e l’apostrofo. Inoltre, la combinazione di BeforeValidator ed Enum sul campo sesso fa sì che input come “m ” o “M” vengano automaticamente normalizzati e validati secondo le regole di business.
Gestione degli errori
Tutta questa configurazione è fantastica, ma dobbiamo ricordarci di gestire anche gli errori. Pydantic solleva un’eccezione specifica: ValidationError.
Invece di lasciare che l’applicazione vada in crash, possiamo intercettare l’errore in modo pulito e capire esattamente cosa è andato storto grazie ai dettagli che Pydantic ci mette a disposizione.
Ecco un esempio:
from pydantic import ValidationError
dati_ricevuti = { "nome": "Mario123", "cognome": "Rossi", "sesso": "X"}
try: utente = PersonaDefinitiva(**dati_ricevuti)except ValidationError as e: print("Ops! I dati inseriti non sono validi.")
for errore in e.errors(): campo = errore['loc'][0] messaggio = errore['msg'] print(f"Errore nel campo '{campo}': {messaggio}")