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

  1. Local dev — env strings are coerced; a non-numeric PORT fails immediately with a clear message.
  2. CI — test both a valid and an invalid value per coerced field to lock in behaviour.
  3. 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() or int() on a raw env string outside the model; let pydantic parse it.
  • Wrap secrets in SecretStr regardless of strictness.

Troubleshooting

  • "0" treated as True — you are calling bool("0") yourself; route the value through the model instead.
  • Float silently truncated — lenient int rejects "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". Remove Strict() 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.