Configuration & Settings
Configuration loads from the environment at startup, validates once with Pydantic 2, and keeps secrets out of source control so the same artifact runs in dev, staging, and production.
Search across all documentation pages
Configuration loads from the environment at startup, validates once with Pydantic 2, and keeps secrets out of source control so the same artifact runs in dev, staging, and production.
Quick-reference recipe card - copy-paste ready.
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
app_name: str = "billing-api"
database_url: str = Field(alias="DATABASE_URL")
debug: bool = False
settings = Settings()When to reach for this:
os.getenv calls# settings.py
from functools import lru_cache
from typing import Literal
from pydantic import Field, PostgresDsn, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
environment: Literal["local", "staging", "production"] = "local"
app_name: str = "orders-api"
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
database_url: PostgresDsn
redis_url: str = "redis://localhost:6379/0"
secret_key: str = Field(min_length=32)
http_timeout_seconds: float = 5.0
@field_validator("secret_key")
@classmethod
def forbid_placeholder_secret(cls, value: str) -> str:
if value in {"changeme", "replace-me"}:
raise ValueError("SECRET_KEY must be set to a real value")
return value
@lru_cache
def get_settings() -> Settings:
return Settings()
# main wiring
import logging
def configure_logging(settings: Settings) -> None:
logging.basicConfig(level=settings.log_level)
def build_app():
settings = get_settings()
configure_logging(settings)
# pass settings.database_url into repository factory
return settings
if __name__ == "__main__":
s = build_app()
print(f"{s.app_name} ready in {s.environment}")# .env.example (committed - no secrets)
DATABASE_URL=postgresql://user:pass@localhost:5432/orders
SECRET_KEY=generate-a-32-char-random-string-for-local-dev
ENVIRONMENT=local
LOG_LEVEL=DEBUGWhat this demonstrates:
.env supports local dev; production relies on real environment variablesget_settings() cached so parsing happens once per processextra="ignore" prevents typos from silently creating attributes.env) into typed fieldsDATABASE_URL env name to database_url attributeget_settings() and passes values into adapters| Factor | Python practice |
|---|---|
| Config in env | BaseSettings, not hardcoded prod URLs |
| Dev/prod parity | same settings class, different env values |
| Secrets | platform secret store → env at runtime |
| Backing services | URLs as settings, swappable per env |
# Nested settings for subsystems
from pydantic_settings import BaseSettings
class S3Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="S3_")
bucket: str
region: str = "us-east-1"
class Settings(BaseSettings):
s3: S3Settings = S3Settings().env with secrets - keys leak via git history. Fix: commit .env.example only; block .env in .gitignore.PostgresDsn, RedisDsn, or custom validators at startup."false" string is truthy in naive if os.getenv("X"). Fix: Pydantic bool parsing handles 0, false, no.settings.debug = True at runtime. Fix: treat settings as immutable after load; use frozen=True models where practical.| Alternative | Use When | Don't Use When |
|---|---|---|
os.environ directly | 10-line scripts | services with dozens of settings |
Django settings.py | Django 5.2 projects | standalone FastAPI/Flask services |
| TOML/YAML config files | static non-secret defaults | secrets that must not live on disk |
python-decouple | legacy projects already using it | greenfield Pydantic stacks |
Production should inject environment variables via the orchestrator or secret manager. .env files on servers are a common leak vector. Local dev may use .env comfortably.
Use SCREAMING_SNAKE_CASE. Prefer field aliases when the external name differs from the Python attribute (DATABASE_URL → database_url).
Use monkeypatch.setenv before calling get_settings.cache_clear() and reinstantiating, or pass a manually constructed Settings object into factories.
Yes - fetch secrets in the composition root and pass them as env vars before constructing Settings, or use a custom settings source (Pydantic v2 custom sources).
Treat flags as settings fields (FEATURE_NEW_CHECKOUT: bool = False). Document defaults in .env.example.
CI sets env vars explicitly in the workflow file. Do not rely on a committed .env for pipeline secrets.
Yes for groups like S3_BUCKET, S3_REGION. Keeps flat env vars organized without one giant flat class.
Pass -e or env_file in compose for non-secrets. Mount secrets from the platform (Kubernetes secrets, ECS secrets) as env vars at container start.
Never for production paths. Local-only defaults belong in .env.example with obvious placeholder values rejected by validators.
Yes. Depends(get_settings) provides settings to routes and dependency factories without global imports.
Document renames in CHANGELOG and support deprecated env names for one release with a validator warning.
Use @computed_field for derived values (e.g. debug mode when environment == "local"). Keep inputs in env, derivations in code.
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.
Reviewed by Chris St. John·Last updated Jul 19, 2026