Pydantic Settings Fundamentals
A BaseSettings subclass is the single object that should own every configuration read in a Python service. It pulls from the environment, applies types, runs validators, and either constructs cleanly or raises a ValidationError that stops the process before it serves traffic. This page builds that object correctly with pydantic-settings v2.
This is the foundation of the type-safe validation section — it turns the raw configuration sources into a typed contract the rest of the application can depend on.
Secure implementation
# config/settings.py
from functools import lru_cache
from pydantic import SecretStr, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
env_nested_delimiter="__", # CACHE__HOST -> cache.host
extra="forbid", # unknown vars raise instead of being ignored
case_sensitive=False,
)
database_url: str
api_key: SecretStr # masked in repr(), model_dump(), and logs
workers: int = Field(default=4, ge=1, le=64)
debug: bool = False
@lru_cache
def get_settings() -> Settings:
return Settings() # constructed once; raises at startup on error
extra="forbid" makes a misspelled variable fatal. SecretStr keeps the API key out of any serialized output. lru_cache means the model is built exactly once and shared.
Configuration reference
| Setting | Type | Default | Security implication |
|---|---|---|---|
extra |
"forbid"/"ignore"/"allow" |
"ignore" |
"forbid" catches typos and injected junk |
env_nested_delimiter |
str |
None |
Enables nested models from flat env vars |
case_sensitive |
bool |
False |
Match your platform’s env-var casing |
SecretStr field |
masked | — | Prevents secret leakage in logs |
env_file |
str |
None |
Local convenience; below real env vars |
Deployment parity: local to production
- Local dev —
.envsupplies values;get_settings()validates them on first call. - CI — instantiate
Settings()in a test; a missing or malformed key fails the build. - Staging/Production — the orchestrator injects environment variables that outrank the (absent)
.env; the identical model validates them at boot.
Security boundaries & guardrails
- Always set
extra="forbid"; silent acceptance of unknown variables hides configuration drift. - Wrap every credential field in
SecretStr; assert in a test thatrepr(settings)contains no secret. - Keep one settings class per service — no per-environment subclasses with diverging fields.
- Use
SettingsConfigDict, not the deprecated innerclass Config.
Troubleshooting
ValidationErrorat startup — a required field is missing or malformed; the message names the exact field. This is the intended fail-fast behaviour.- Nested model not populated —
env_nested_delimiteris unset or the variable uses the wrong delimiter (CACHE__HOST, notCACHE_HOST). See Nested Settings Models in Pydantic. - Secret printed in logs — the field is a plain
str; change it toSecretStr. - Migrating from v1 — inner
class ConfigandBaseSettingsfrompydanticmoved; see Migrate to pydantic-settings v2.
Frequently asked questions
How do I configure a BaseSettings model in pydantic v2?
Use model_config = SettingsConfigDict(...) on the class. The legacy inner class Config from v1 still partly works but raises deprecation warnings; SettingsConfigDict is the supported v2 API.
In what order does pydantic-settings read sources?
By default, init arguments win, then OS environment variables, then the .env file, then file secrets. Environment variables outrank the .env file, giving you correct production parity automatically.
How do I reject unknown environment variables?
Set extra="forbid" in SettingsConfigDict. A typo’d or stray variable then raises a ValidationError at startup instead of being silently ignored.
Conclusion
The invariant: one BaseSettings model, extra="forbid", secrets as SecretStr, constructed once and validated at startup. Everything else in this section extends this object.