Chapter 21 · Configuration Management and Application Lifecycle
Chapter goals: Manage environment variables with pydantic-settings, and gracefully manage app-level resource startup and teardown with lifespan.
21.1 Why Configuration Management Is Needed
Values like database URLs, secrets, and third-party credentials: they vary by environment (dev/test/prod differ), and are often sensitive — cannot be written into the code repository. The industry standard is to put them in environment variables, read by the app at runtime.
But environment variables have a天然 defect — they are always strings:
import os
items_per_user = os.getenv("ITEMS_PER_USER") # "50" not 50
# Type conversion and validation must be hand-written; miss one place and it is a production incident21.2 pydantic-settings: Validated Configuration
pydantic-settings brings Pydantic's type coercion and validation to environment variables:
uv add pydantic-settings python-dotenv# config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
app_name: str = "Awesome API" # Can omit if a default exists
admin_email: str
items_per_user: int = 50 # Auto-converts from string to int and validates
database_url: str = "sqlite:///./app.db"
# Read from .env file in project root (requires python-dotenv)
model_config = SettingsConfigDict(env_file=".env")
settings = Settings() # Reads env vars on instantiation; case-insensitive matching# .env (remember to add to .gitignore!)
ADMIN_EMAIL="admin@example.com"
APP_NAME="ChimichangApp"Resolution priority: environment variables > .env file > defaults; env var names match field names case-insensitively (APP_NAME → app_name). Override temporarily at startup:
ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.pyPut config in a separate module
Place Settings in config.py (paired with Chapter 19's package structure);各处 from .config import settings. Never commit .env to Git.
21.3 Settings as a Dependency + @lru_cache Singleton
The problem with a bare global settings object: it is hard to replace in tests. The more elegant approach is to make config a dependency:
# config.py —— Note: no global instance created here
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
app_name: str = "Awesome API"
admin_email: str
items_per_user: int = 50
model_config = SettingsConfigDict(env_file=".env")# main.py
from functools import lru_cache
from fastapi import Depends, FastAPI
from .config import Settings
app = FastAPI()
@lru_cache # Critical: create Settings exactly once per process
def get_settings() -> Settings:
return Settings()
@app.get("/info")
def info(settings: Settings = Depends(get_settings)):
return {"app_name": settings.app_name}Why @lru_cache? Without it, def get_settings(): return Settings() would re-instantiate Settings and re-read the .env file from disk on every request — file I/O is a slow operation, completely unnecessary. With @lru_cache, the first call executes and caches the result; all subsequent requests reuse the same object.
The biggest benefit of using it as a dependency is test-friendliness:
def test_info_with_mocked_settings(client, monkeypatch):
mock_settings = Settings(app_name="TestApp")
app.dependency_overrides[get_settings] = lambda: mock_settings
resp = client.get("/info")
assert resp.json()["app_name"] == "TestApp"21.4 Lifespan: Graceful Startup and Shutdown
The lifespan context manager replaces the old on_event("startup") / on_event("shutdown") pattern:
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: initialize resources
print("Starting up...")
db_engine = create_engine("postgresql://...")
app.state.db = db_engine
yield
# Shutdown: clean up resources
print("Shutting down...")
db_engine.dispose()
app = FastAPI(lifespan=lifespan)Access the initialized resource in dependencies:
def get_db(request: Request):
return request.app.state.db21.5 Chapter Summary
pydantic-settings+BaseSettingsfor typed, validated environment variable config;@lru_cacheon the settings dependency prevents re-reading .env on every request;dependency_overridesmakes settings replaceable in tests;lifespancontext manager replaces on_event for graceful resource lifecycle management.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. Why use pydantic-settings instead of os.getenv()?
2. Why decorate get_settings with @lru_cache?
3. What is the advantage of lifespan over on_event("startup")?
4. How do you replace Settings in a test?
🛠️ Hands-On Exercises
- Create a
config.pywithSettings(BaseSettings)reading from.env; access it via aget_settingsdependency in a route. - Add
@lru_cachetoget_settings; add a print statement to verify Settings is instantiated only once. - Write a test that overrides
get_settingswith a mock Settings and verifies the route returns the mocked values.
Proceed to Chapter 22: Production Deployment.