Cheatsheet for app config. Pairs with Pydantic v2 Textbook Ch 9 (Settings) .
Basic Settings
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field
class Settings(BaseSettings):
env: str = "dev"
database_url: str
secret_key: str
log_level: str = "INFO"
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
env_prefix="MYAPP_",
case_sensitive=False,
extra="ignore",
)
Use as a Depends
from functools import lru_cache
from fastapi import Depends
@lru_cache
def get_settings() -> Settings:
return Settings()
@app.get("/info")
async def info(s: Settings = Depends(get_settings)):
return {"env": s.env}
lru_cache ensures one instance.
Nested settings
class DBSettings(BaseSettings):
url: str
pool_size: int = 10
model_config = SettingsConfigDict(env_prefix="MYAPP_DB_")
class Settings(BaseSettings):
db: DBSettings = DBSettings()
secret_key: str
model_config = SettingsConfigDict(env_prefix="MYAPP_")
env_nested_delimiter
MYAPP_DB__URL=postgresql://...
MYAPP_DB__POOL_SIZE=20
class Settings(BaseSettings):
db: DBSettings
model_config = SettingsConfigDict(env_nested_delimiter="__")
Secrets directory (Docker)
class Settings(BaseSettings):
secret_key: str
model_config = SettingsConfigDict(secrets_dir="/run/secrets")
Reads /run/secrets/secret_key. Used with K8s Secrets mounted as files.
SecretStr (no logging leaks)
from pydantic import SecretStr
class Settings(BaseSettings):
api_key: SecretStr
s = Settings()
print(s.api_key) # SecretStr('**********')
print(s.api_key.get_secret_value()) # actual value
Multiple env files
model_config = SettingsConfigDict(env_file=(".env", ".env.local"))
Later overrides earlier.
Validation across fields
from pydantic import model_validator
class Settings(BaseSettings):
env: Literal["dev", "staging", "prod"] = "dev"
database_url: str
@model_validator(mode="after")
def no_localhost_in_prod(self):
if self.env == "prod" and "localhost" in self.database_url:
raise ValueError("prod can't use localhost DB")
return self
CLI integration
class Settings(BaseSettings):
database_url: str
debug: bool = False
model_config = SettingsConfigDict(cli_parse_args=True)
# python -m myapp --database-url=... --debug
TOML / YAML / JSON sources
from pydantic_settings import TomlConfigSettingsSource
class Settings(BaseSettings):
model_config = SettingsConfigDict(toml_file="config.toml")
@classmethod
def settings_customise_sources(cls, settings_cls, *args):
return (TomlConfigSettingsSource(settings_cls),) + args
Per-environment loading
import os
env = os.environ.get("MYAPP_ENV", "dev")
env_file = f".env.{env}"
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=(".env", env_file), env_prefix="MYAPP_")
Lifespan validation
@asynccontextmanager
async def lifespan(app):
try:
s = get_settings()
log.info("settings_loaded", env=s.env)
except ValidationError as e:
log.error("invalid_settings", err=str(e))
raise
yield
Fail fast if env vars missing.
Override in tests
@pytest.fixture
def override_settings(monkeypatch):
monkeypatch.setenv("MYAPP_DATABASE_URL", "postgresql+asyncpg://test")
monkeypatch.setenv("MYAPP_SECRET_KEY", "test")
get_settings.cache_clear()
Don’t commit .env
.env
.env.local
*.env.local
Commit .env.example instead, with placeholders.
Read this next
If you want my Pydantic Settings + Vault + ESO reference, 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 .