You reached for Pydantic to stop bad data at the door. Yet somehow strings become numbers, empty values sail through, and downstream code still explodes. Pydantic did exactly what you told it to โ the problem is what you told it.
The Symptom
A model that looks strict:
from pydantic import BaseModel class Order(BaseModel): quantity: int coupon: str = "" Order(quantity="5") # passes โ quantity is now 5 Order(quantity=True) # passes โ quantity is now 1
You expected "5" and True to be rejected. Instead Pydantic coerced them.
Why It Happens
By default Pydantic v2 runs in lax mode: it will convert compatible types rather than reject them. A string that looks like a number becomes a number; a bool becomes an int. This is convenient for parsing JSON from HTTP, but it means "typed" is not the same as "validated."
The empty-string default is a second trap. coupon: str = "" makes the field optional and silently accepts blanks as valid.
The Fix
Turn on strict mode where coercion is unwanted:
from pydantic import BaseModel, StrictInt, Field class Order(BaseModel): quantity: StrictInt = Field(gt=0) coupon: str | None = Field(default=None, min_length=3) Order(quantity="5") # now raises ValidationError Order(quantity=0) # raises โ gt=0 constraint
StrictInt refuses non-int inputs, and Field(gt=0) enforces a real business rule instead of just a type.
Validate the values, not just the shapes. Use field constraints (min_length, max_length, ge, le, pattern) and custom validators for anything with meaning:
from pydantic import field_validator class Order(BaseModel): email: str @field_validator("email") @classmethod def must_have_at(cls, v: str) -> str: if "@" not in v: raise ValueError("invalid email") return v
Prevention
- โธDecide per field whether coercion is acceptable; use
Strict*types where it is not. - โธReplace bare
= ""and= Nonedefaults with explicit constraints. - โธSet
model_config = ConfigDict(extra="forbid")so unexpected keys are rejected instead of ignored.
The takeaway: Pydantic validates against the rules you declare, and a plain type annotation is a weak rule. Strict types plus real constraints turn a model from a shape hint into an actual gate.