Strict Mode & Type Coercion
Pydantic’s coercion is a feature and a trap. It is a feature because environment variables are always strings and you want "8080" to become an int. It is a trap because the same leniency can turn "0" into a truthy value somewhere you needed a strict boundary. This page makes coercion an explicit, per-field decision.
Coercion control is the layer between settings fundamentals and custom validators in the type-safe validation section.
Secure implementation
# config/strict_settings.py
from typing import Annotated
from pydantic import Field, Strict, SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class StrictSettings(BaseSettings):
model_config = SettingsConfigDict(extra="forbid")
# Lenient: env var "8080" is coerced to int 8080.
port: int = Field(default=8080, ge=1, le=65535)
# Lenient bool: "true"/"false"/"1"/"0"/"yes"/"no" parsed correctly by pydantic.
debug: bool = False
# Strict: must already be the right type — useful when fed from JSON, not env.
replicas: Annotated[int, Strict()] = 3
secret_key: SecretStr
Most env-sourced fields should stay lenient; reserve Strict() for values that arrive already-typed (from a parsed JSON document) where a silent string-to-number coercion would mask a bug.
Configuration reference
| Input | Target | Lenient result | Strict result |
|---|---|---|---|
"8080" |
int |
8080 |
ValidationError |
"true" |
bool |
True |
ValidationError |
"false" |
bool |
False |
ValidationError |
1 |
bool |
True |
ValidationError |
"1.5" |
int |
ValidationError |
ValidationError |
Deployment parity: local to production
- Local dev — env strings are coerced; a non-numeric
PORTfails immediately with a clear message. - CI — test both a valid and an invalid value per coerced field to lock in behaviour.
- Staging/Production — identical coercion rules mean a value that validated in CI validates at boot.
Security boundaries & guardrails
- Decide coercion per field deliberately; do not flip the whole model to strict on a whim.
- Range-check coerced numbers with
Field(ge=, le=)— coercion does not bound the value. - Keep
extra="forbid"so unexpected keys never reach a coercion path. - Never call
bool()orint()on a raw env string outside the model; let pydantic parse it. - Wrap secrets in
SecretStrregardless of strictness.
Troubleshooting
"0"treated asTrue— you are callingbool("0")yourself; route the value through the model instead.- Float silently truncated — lenient
intrejects"1.5"; if you need truncation, validate and convert explicitly. - Strict field rejects an env var — env vars are strings; a
Strict()int cannot accept"3". RemoveStrict()for env-sourced fields. - Inconsistent behaviour across versions — pin pydantic v2; see Migrate to pydantic-settings v2.
Frequently asked questions
Does pydantic coerce environment variable strings automatically?
Yes. Because environment variables are always strings, pydantic-settings coerces "8080" to int 8080 and "true" to bool True by default. That is usually what you want for env vars, but you can opt into strict mode per field.
How do I make a single field strict while leaving others lax?
Annotate it with Strict() — port: Annotated[int, Strict()] — or set strict=True on the Field. The rest of the model keeps its lenient coercion.
Why does “False” become True for a bool field?
That happens only with the built-in bool(), not pydantic. Pydantic correctly parses "false", "0", "no", and "off" to False. If you see the bug, you are bypassing the model and calling bool() on the raw string.
Conclusion
The invariant: coercion is a per-field decision, lenient for env strings and strict for already-typed inputs, with explicit range checks on every number. Never reimplement parsing outside the model.