FastAPI config via pydantic-settings as a cached dependency: working versions (tested 2026-10-15)
Load config from env vars with pydantic-settings, inject it into FastAPI routes through a cached dependency, and test it offline with monkeypatch and dependency_overrides.
passed5 passed · 0 failed2026-10-15 13:29 UTC- Status
- passed
- Last passed
- 2026-10-15 13:29 UTC
- Tests
- 5 / 5
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38742087302
The code
Exactly the files that passed. Your agent can pull them with get_recipe("fastapi-pydantic-settings-dependency") (install), or see them on GitHub.
fastapi==0.143.1 pydantic-settings==2.16.0 httpx==0.28.1 pytest==9.1.1
from functools import lru_cache
from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(case_sensitive=False)
app_name: str = "demo"
debug: bool = False
database_url: str = "sqlite:///:memory:"
api_key: SecretStr # required: no default, so a missing env var fails loudly
@lru_cache
def get_settings() -> Settings:
# Built once per process. Env changes are invisible until cache_clear().
return Settings()
from typing import Annotated
from fastapi import Depends, FastAPI, Header, HTTPException
from config import Settings, get_settings
app = FastAPI()
SettingsDep = Annotated[Settings, Depends(get_settings)]
@app.get("/info")
def info(settings: SettingsDep):
return {"app": settings.app_name, "debug": settings.debug}
@app.get("/secure")
def secure(settings: SettingsDep, x_api_key: Annotated[str | None, Header()] = None):
if x_api_key != settings.api_key.get_secret_value():
raise HTTPException(status_code=401, detail="bad key")
return {"ok": True}
import pytest
from fastapi.testclient import TestClient
from pydantic import ValidationError
from config import Settings, get_settings
from main import app
@pytest.fixture(autouse=True)
def fresh_settings():
get_settings.cache_clear()
yield
app.dependency_overrides.clear()
get_settings.cache_clear()
@pytest.fixture
def client():
return TestClient(app)
def test_env_vars_fill_settings(monkeypatch, client):
monkeypatch.setenv("API_KEY", "env-key")
monkeypatch.setenv("APP_NAME", "from-env")
monkeypatch.setenv("DEBUG", "true")
assert client.get("/info").json() == {"app": "from-env", "debug": True}
def test_cache_pins_first_read(monkeypatch):
monkeypatch.setenv("API_KEY", "first")
first = get_settings()
monkeypatch.setenv("API_KEY", "second")
assert get_settings() is first
assert get_settings().api_key.get_secret_value() == "first"
get_settings.cache_clear()
assert get_settings().api_key.get_secret_value() == "second"
def test_override_replaces_cached_dependency(client):
# The override key must be the exact function object used in Depends().
app.dependency_overrides[get_settings] = lambda: Settings(
api_key="k", app_name="override", _env_file=None
)
assert client.get("/info").json()["app"] == "override"
def test_secure_uses_overridden_key(client):
app.dependency_overrides[get_settings] = lambda: Settings(api_key="s3cret", _env_file=None)
assert client.get("/secure", headers={"x-api-key": "s3cret"}).status_code == 200
assert client.get("/secure", headers={"x-api-key": "nope"}).status_code == 401
def test_missing_required_key_fails(monkeypatch):
monkeypatch.delenv("API_KEY", raising=False)
with pytest.raises(ValidationError):
Settings(_env_file=None)
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
FastAPI endpoint with Pydantic v2 discriminated unions and Annotated validators
Accept a tagged union of payment methods with Field(discriminator), add Annotated validators, and test the exact 422 error shapes offline with TestClient.
FastAPI endpoints rate limited with slowapi, tested with TestClient
Add per-route and per-API-key rate limits to FastAPI with slowapi, then test 429 behavior offline with TestClient and the in-memory store.
FastAPI Jinja2 full pages vs HTMX partials via HX-Request
Serve a full HTML page to normal browsers and a bare HTML fragment to HTMX requests, chosen by the HX-Request header, with Vary set for caches. Tested offline with the sync TestClient.
FastAPI lifespan state tested with httpx ASGITransport and asgi-lifespan
A FastAPI app that opens a SQLite connection in its lifespan, then test it offline through httpx ASGITransport, with asgi-lifespan running startup and shutdown.
All FastAPI recipesAll Pydantic Settings recipesAll httpx recipes