Settings & Configuration — pydantic-settings and .env
IntermediateBaseSettings reads typed config from environment variables and .env files — validated at startup, injected via a cached get_settings dependency, with secrets kept out of git entirely.
Overview
Hard-coded config is how staging code talks to production databases. The twelve-factor rule is config-from-environment, and pydantic-settings makes it typed: a Settings class declares every knob (DATABASE_URL, JWT_SECRET, DEBUG) with types and defaults; values load from real environment variables first, then a .env file for local development. Validation runs at import — a missing DATABASE_URL crashes the app at startup with a clear message instead of at 2 a.m. on the first request. Expose settings through an @lru_cache-ed get_settings() dependency: endpoints declare what they need, and tests can override the whole configuration without touching the environment. The .env file itself never enters git — .env.example with dummy values documents the contract.
Typed Settings from Environment + .env
Declare once, validate at startup. Environment variables beat .env values, which beat defaults — exactly the precedence deployment needs (Fly/Render/K8s secrets override local files).
# pip install pydantic-settings
# ── app/core/config.py ──────────────────────
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env")
app_name: str = "Placement API"
env: str = "dev" # dev | staging | prod
debug: bool = False
database_url: str # REQUIRED — no default, crash if absent
jwt_secret: str # REQUIRED
jwt_ttl_minutes: int = 30
razorpay_key_id: str | None = None # optional integration
allowed_origins: list[str] = ["http://localhost:3000"]
# ── .env (local dev ONLY — in .gitignore) ───
# DATABASE_URL=postgresql://app:app@localhost:5432/placement
# JWT_SECRET=dev-only-not-for-prod
# DEBUG=true
# ALLOWED_ORIGINS=["http://localhost:3000","http://localhost:3001"]
# ── .env.example (committed — the contract, dummy values) ──
# DATABASE_URL=postgresql://user:pass@host:5432/dbname
# JWT_SECRET=change-me
# Missing DATABASE_URL at startup →
# pydantic ValidationError: database_url: Field required
# The app refuses to boot half-configured. That is a feature.get_settings() — Cached and Injectable
A cached factory makes settings a dependency like any other: endpoints stay testable, and lru_cache means the .env parse happens once, not per request. Branch behaviour on settings.env, never on hostname guessing.
from functools import lru_cache
from fastapi import Depends, FastAPI
from app.core.config import Settings
@lru_cache # parse env/.env exactly once
def get_settings() -> Settings:
return Settings()
app = FastAPI()
@app.get("/info")
def info(settings: Settings = Depends(get_settings)):
return {
"app": settings.app_name,
"env": settings.env,
# NEVER echo jwt_secret / database_url in any endpoint
}
# Environment-dependent behaviour, the honest way:
def make_app() -> FastAPI:
s = get_settings()
return FastAPI(
title=s.app_name,
docs_url=None if s.env == "prod" else "/docs", # docs off in prod
debug=s.debug,
)
# Tests: swap the ENTIRE config without touching os.environ —
# app.dependency_overrides[get_settings] = lambda: Settings(
# database_url="sqlite:///test.db", jwt_secret="test", env="test"
# )
# Production secrets live in the platform, not in files:
# fly secrets set JWT_SECRET=... (or K8s Secrets / AWS SSM)
# Rotation = update the secret + restart. No commit, no redeploy of code.Key Points to Remember
- 1BaseSettings: typed, validated config from env vars + .env (env wins)
- 2Required fields without defaults make missing config a startup crash
- 3@lru_cache get_settings() + Depends = injectable, test-overridable config
- 4.env is gitignored; .env.example documents; prod secrets live in the platform
Interview Questions
Sign in to ask AriaHow do you manage config differences between dev, staging, and prod in FastAPI?
Why should the app crash at startup if DATABASE_URL is missing rather than default to localhost?
A developer committed .env with production credentials — walk through the incident response.
Ask Aria about Settings & Configuration — pydantic-settings and .env
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.