skip to content
OphusDev

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 base
person_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 sesso
person_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 Enum
from typing import Annotated
from pydantic import BaseModel, BeforeValidator, StringConstraints
class PersonaSessoEnum(str, Enum):
M = "M"
F = "F"
# Tipo personalizzato per i nomi validi
ValidName = 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 sesso
ValidSex = Annotated[
PersonaSessoEnum,
BeforeValidator(lambda v: v.upper().strip() if isinstance(v, str) else v),
]
class PersonaDefinitiva(BaseModel):
nome: ValidName
cognome: ValidName
sesso: ValidSex

Grazie 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}")