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: int accepts True.
  • 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 .