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 .