Chapter 7: strict mode. The single biggest semantic choice in Pydantic.
Default: lax
class M(BaseModel):
age: int
M.model_validate({"age": "25"}) # → age=25 (coerced)
M.model_validate({"age": 25.0}) # → age=25 (coerced)
M.model_validate({"age": True}) # → age=1 (coerced; bool is subclass of int!)
Lax mode coerces compatible types. Useful when input is JSON / query params (strings) but you want native types.
Strict per model
class M(BaseModel):
model_config = {"strict": True}
age: int
M.model_validate({"age": "25"}) # ValidationError
No coercion. Type must match exactly.
Strict per field
from pydantic import StrictInt, StrictStr, StrictFloat, StrictBool
class M(BaseModel):
age: StrictInt
name: StrictStr
Or via Annotated:
from typing import Annotated
from pydantic import Strict
class M(BaseModel):
age: Annotated[int, Strict()]
Per-call strict
M.model_validate({"age": "25"}, strict=True)
Override at call time without changing the model.
Strict in JSON
For model_validate_json:
M.model_validate_json('{"age": "25"}', strict=True)
Note: JSON itself doesn’t coerce; “25” is a string in JSON. Strict mode here checks “string in JSON → int in model” doesn’t auto-coerce.
When to use strict
- Internal RPC where types are guaranteed.
- Strict API contracts where type-mismatch should fail loudly.
- Tests to catch unintended coercion.
When NOT:
- Query parameters (always strings).
- Form data (always strings).
- CSV parsing.
- Loose external APIs.
For FastAPI: lax for request bodies (good UX); strict for internal services if you control both ends.
Coercion rules in lax mode
For int:
str→ if numeric, parse.bool→ 0 or 1 (gotcha:Truebecomes1).float→ if integer-valued, accept; else error.
For float:
str→ if numeric, parse.int→ accept.bool→ 0.0 or 1.0.
For str:
- Most types → str(…). Note:
True→ “True”, not “true”.
For bool:
0,1→ False, True."true","false","yes","no","on","off"→ bool.- Lots of variants. See pydantic docs.
For datetime:
- ISO 8601 string → datetime.
- Unix timestamp number → datetime (PG, naive).
- Strict: only datetime.
Bool is a subclass of int
class M(BaseModel):
age: int
M.model_validate({"age": True}) # age=1 (yes!)
Python True == 1. Pydantic accepts. To reject: use Strict or:
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
JSON-specific behavior
model_validate_json parses JSON with stricter rules than model_validate(dict):
class M(BaseModel):
age: int
M.model_validate({"age": "25"}) # → 25 (lax dict)
M.model_validate_json('{"age": "25"}') # → ValidationError (JSON doesn't coerce strings)
JSON has clear types; Pydantic respects them.
Constrained types and coercion
class M(BaseModel):
age: int = Field(ge=0)
M.model_validate({"age": "25"}) # → age=25 (after coercion); passes ge=0
M.model_validate({"age": "-5"}) # → -5 (after coercion); fails ge=0
Coerce first; then constraints.
Strict on root
from pydantic import RootModel
class Names(RootModel[list[str]]):
model_config = {"strict": True}
Common mistakes
1. Strict for query params
Query params always come as strings. Strict rejects them. Use lax for FastAPI request validation.
2. Forgetting bool is int
age: int accepts True as 1. If unwanted, strict or pre-validator.
3. Lax in internal code
Service A sends int; Service B accepts int as str by accident. Strict between services helps catch protocol mismatch.
4. Mixing modes inconsistently
Some models strict; others lax; same data flows through. Pick one mode per layer.
5. Not testing edge cases
"True", "FALSE", 0, "0" — all coerce differently. Tests for the actual inputs you’ll see.
What’s next
Chapter 8: JSON Schema generation.
Read this next
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 .