Configuration Management in FastAPI
Configuration management is the practice of loading every environment-specific input — database URLs, feature flags, secret references — into a single typed object that is validated once and injected everywhere it is needed.
Configuration is the input to the application factory: the factory accepts a settings object and wires the app around it. Treating configuration as a first-class, typed dependency is what makes a service testable across environments and safe to deploy, and it connects directly to Dependency Injection Strategies, since settings are delivered through the same injection mechanism as everything else. This topic sits within Core Architecture and Routing Patterns.
Core Mechanics: Typed Settings with Precedence
pydantic-settings resolves configuration from multiple sources with a defined precedence: explicit arguments override environment variables, which override .env values, which override field defaults. Because the result is a Pydantic model, every field is parsed and validated — a malformed DATABASE_URL or an out-of-range pool size is rejected immediately rather than failing deep in a request.
# app/config.py
from functools import lru_cache
from pydantic import PostgresDsn, SecretStr, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_prefix="APP_",
extra="forbid", # Reject unknown variables so typos fail loudly.
)
project_name: str = "api-service"
environment: str = Field(pattern="^(development|staging|production|test)$")
database_url: PostgresDsn # Required — no default, so it must be supplied.
secret_key: SecretStr = Field(min_length=32) # Never printed in logs or repr.
db_pool_size: int = Field(default=10, ge=2, le=50)
enable_signup: bool = True
@lru_cache # Parse once per process; all consumers share the instance.
def get_settings() -> Settings:
return Settings()
Production Implementation: Inject, Don't Import
The cached get_settings function is the seam between configuration and the rest of the app. Inject it as a dependency so any handler or service receives settings as a parameter, which means tests can override it with a different configuration through app.dependency_overrides — no environment juggling required.
from typing import Annotated
from fastapi import APIRouter, Depends
from app.config import Settings, get_settings
router = APIRouter()
@router.get("/features")
async def features(settings: Annotated[Settings, Depends(get_settings)]) -> dict[str, bool]:
# Configuration arrives as a typed argument, not a global lookup.
return {"signup_enabled": settings.enable_signup}
Async and Performance Notes
get_settings is wrapped in lru_cache, so parsing and validation happen exactly once and every subsequent resolution is a dictionary hit. This matters because settings are read on hot paths; an uncached settings construction that re-reads .env on every request would add filesystem latency to every call. The cache also gives you a single, predictable object identity, which is convenient when other dependencies close over it.
Testing Strategy
Override the settings dependency to exercise environment-specific behavior without touching the real environment:
def test_signup_disabled(client_factory):
app = client_factory()
app.dependency_overrides[get_settings] = lambda: Settings(
environment="test", database_url="postgresql://x/y", secret_key="x" * 32,
enable_signup=False,
)
# ... assert the signup route now returns 403.
Failure Modes and Debugging
- Silent misconfiguration. Without required fields, a missing variable produces a wrong-but-running service. Declare required fields without defaults so boot fails instead.
- Secrets in logs. Plain
strsecret fields leak into tracebacks and structured logs. UseSecretStrand access the value explicitly with.get_secret_value()only where needed. - Per-request re-parsing. Constructing
Settings()inside a handler re-reads sources every call. Always go through the cached accessor. - Environment drift. Different variable names across environments cause works-here-fails-there bugs. The detailed approach to taming this is in Managing Environment Variables with Pydantic Settings.
Related Reading
- Up to the section: Core Architecture and Routing Patterns.
- Hands-on guide: Managing Environment Variables with Pydantic Settings.
- Composes with: Application Factory Patterns and Dependency Injection Strategies.