Cheatsheet for Pydantic strictness.
Lax (default)
Pydantic coerces compatible types:
class M(BaseModel):
age: int
M.model_validate({"age": "25"}) # coerced to 25
M.model_validate({"age": 25.0}) # coerced to 25
M.model_validate({"age": True}) # → 1 (bool is int subclass)
Strict mode (per-model)
class M(BaseModel):
model_config = {"strict": True}
age: int
M.model_validate({"age": "25"}) # ValidationError
Strict per-field
from pydantic import StrictInt, StrictStr, StrictBool, StrictFloat
class M(BaseModel):
age: StrictInt
name: StrictStr
Or:
from typing import Annotated
from pydantic import Strict
class M(BaseModel):
age: Annotated[int, Strict()]
Strict per-call
M.model_validate({"age": "25"}, strict=True)
When to use strict
- Internal service-to-service RPC (types are known).
- Tests catching unintended coercion.
- Strict API contracts.
When NOT:
- Query parameters (always arrive as strings).
- Form data.
- CSV / loose external inputs.
JSON input behaves stricter than dict
class M(BaseModel):
age: int
M.model_validate({"age": "25"}) # lax → 25
M.model_validate_json('{"age": "25"}') # strict → error (JSON has types)
JSON has explicit types; Pydantic respects them.
Bool is int subclass (Python gotcha)
class M(BaseModel):
age: int
M.model_validate({"age": True}) # age=1 in lax mode
To reject:
class M(BaseModel):
age: int
@field_validator("age", mode="before")
@classmethod
def reject_bool(cls, v):
if isinstance(v, bool):
raise ValueError("must be int, not bool")
return v
Or use StrictInt.
Coercion rules (lax)
For int:
str→ if numeric, parse.bool→ 0/1.float→ if integer-valued, accept.
For bool:
0,1→ False, True."true","false","yes","no","on","off".
For datetime:
- ISO string → datetime.
- Unix timestamp → datetime.
Per-field with Annotated
from typing import Annotated
from pydantic import Strict
class M(BaseModel):
age: Annotated[int, Strict()] # only int
code: Annotated[str, Strict()] # only str (not int → "1")
Constraints after coercion
class M(BaseModel):
age: int = Field(ge=0)
M.model_validate({"age": "25"}) # → 25 then ge check
M.model_validate({"age": "-5"}) # → -5, then fails ge=0
Lax mode coerces first; constraints check after.
Strict on root
class Names(RootModel[list[str]]):
model_config = {"strict": True}
Strict only for specific cases
# Default lax
class UserCreate(BaseModel):
email: str
age: int
# Strict variant
class UserCreateStrict(UserCreate):
model_config = {"strict": True}
Or just strict=True in model_validate(...).
Common mistakes
- Strict everywhere — query params arrive as strings; rejects them.
- Lax in service-to-service — protocol mismatches sneak in.
- Forgetting bool-is-int —
age: intacceptsTrue. - Different strictness for input vs JSON — surprising behavior.
Read this next
If you want my strict-mode patterns for service-to-service APIs, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .