FastAPI Settings & Configuration: Environment Guide
Configuration management separates your code from its environment. A single FastAPI codebase should run identically in development, testing, and production with only configuration changing. The pydantic-settings library lets you define configuration as a Pydantic model, automatically loading from environment variables, .env files, or defaults. This guide shows you how to structure multi-environment configuration so your API runs reliably everywhere without changing code.
I've seen production incidents caused by misconfigured database URLs or API keys. This article teaches you patterns that prevent configuration errors and make deployments predictable.
Setting Up pydantic-settings
First, install pydantic-settings:
pip install pydantic-settings
Then define your configuration as a model:
from pydantic_settings import BaseSettings
from typing import Optional
class Settings(BaseSettings):
"""Application configuration loaded from environment."""
# Database
database_url: str = "sqlite:///./test.db"
database_pool_size: int = 5
# Server
api_title: str = "My API"
api_version: str = "1.0.0"
debug: bool = False
# Security
secret_key: str
access_token_expire_minutes: int = 30
# External services
external_api_url: str
external_api_key: str
class Config:
env_file = ".env" # Load from .env file
env_file_encoding = "utf-8"
case_sensitive = False
settings = Settings()
Create a .env file in your project root:
DATABASE_URL=postgresql+asyncpg://user:password@localhost/mydb
SECRET_KEY=your-secret-key-here
EXTERNAL_API_URL=https://api.example.com
EXTERNAL_API_KEY=external-key
DEBUG=false
Now settings.database_url is automatically populated from the environment variable. If the variable isn't set, it falls back to the class default (if provided).
Environment-Specific Configuration
Use a base Settings class and subclass it per environment:
from pydantic_settings import BaseSettings
from typing import Literal
class BaseSettings(BaseSettings):
"""Shared configuration across all environments."""
api_title: str = "My API"
api_version: str = "1.0.0"
class Config:
env_file = ".env"
case_sensitive = False
class DevelopmentSettings(BaseSettings):
"""Development environment configuration."""
environment: Literal["development"] = "development"
debug: bool = True
database_url: str = "sqlite:///./dev.db"
log_level: str = "DEBUG"
class Config:
env_file = ".env.development"
class TestingSettings(BaseSettings):
"""Testing environment configuration."""
environment: Literal["testing"] = "testing"
debug: bool = True
database_url: str = "sqlite:///:memory:"
log_level: str = "DEBUG"
class Config:
env_file = ".env.testing"
class ProductionSettings(BaseSettings):
"""Production environment configuration."""
environment: Literal["production"] = "production"
debug: bool = False
database_url: str # Required; no default
secret_key: str # Required; must be set
log_level: str = "INFO"
class Config:
env_file = ".env.production"
def get_settings() -> BaseSettings:
"""Return settings based on ENVIRONMENT variable."""
env = os.getenv("ENVIRONMENT", "development")
if env == "testing":
return TestingSettings()
elif env == "production":
return ProductionSettings()
else:
return DevelopmentSettings()
settings = get_settings()
Now set the ENVIRONMENT variable to control which settings class is used:
# Development
export ENVIRONMENT=development
python -m uvicorn main:app --reload
# Production
export ENVIRONMENT=production
python -m uvicorn main:app
Validating Configuration at Startup
Validate critical settings when the app starts:
from pydantic import field_validator, HttpUrl
class ProductionSettings(BaseSettings):
environment: Literal["production"] = "production"
database_url: str
secret_key: str
external_api_url: HttpUrl
@field_validator("secret_key")
@classmethod
def validate_secret_key(cls, v: str) -> str:
"""Ensure secret key is long enough."""
if len(v) < 32:
raise ValueError("secret_key must be at least 32 characters")
return v
@field_validator("database_url")
@classmethod
def validate_database_url(cls, v: str) -> str:
"""Ensure database URL is not SQLite in production."""
if "sqlite" in v.lower():
raise ValueError("SQLite is not allowed in production")
return v
settings = ProductionSettings() # Raises ValueError if validation fails
Validation at startup catches misconfigurations immediately, preventing silent failures.
Loading Secrets from External Services
For production, load secrets from a secrets manager instead of .env:
import boto3
import json
from pydantic_settings import BaseSettings
class ProductionSettings(BaseSettings):
database_url: str
secret_key: str
@classmethod
def settings_customizer(cls, settings):
"""Load secrets from AWS Secrets Manager."""
client = boto3.client("secretsmanager", region_name="us-east-1")
try:
response = client.get_secret_value(SecretId="myapp/prod")
secret = json.loads(response["SecretString"])
settings.database_url = secret["database_url"]
settings.secret_key = secret["secret_key"]
except Exception as e:
raise ValueError(f"Failed to load secrets: {e}")
return settings
class Config:
# Load from environment first, then customize
env_file = ".env"
settings = ProductionSettings().settings_customizer()
Or use environment variables directly (the recommended production pattern):
# In your deployment (K8s, Heroku, etc.), set environment variables
export DATABASE_URL=postgresql://...
export SECRET_KEY=...
export EXTERNAL_API_KEY=...
# Your FastAPI app reads them automatically
Using Settings in Routes and Lifespan
Inject settings into routes via dependencies:
from fastapi import FastAPI, Depends
app = FastAPI(
title=settings.api_title,
version=settings.api_version,
debug=settings.debug
)
def get_settings() -> Settings:
"""Dependency to inject settings."""
return settings
@app.get("/config")
async def get_config(settings: Settings = Depends(get_settings)):
"""Return non-sensitive configuration."""
return {
"api_version": settings.api_version,
"environment": settings.environment
}
Or access settings from app context in lifespan:
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
# Initialize database with settings
app.db_url = settings.database_url
app.db_pool = await create_pool(app.db_url)
yield
await app.db_pool.close()
app = FastAPI(lifespan=lifespan)
Logging Configuration
Use settings to control logging levels per environment:
import logging.config
class Settings(BaseSettings):
log_level: str = "INFO"
def configure_logging(self):
"""Set up logging based on settings."""
logging.basicConfig(
level=self.log_level,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
# Suppress noisy loggers in production
if self.environment == "production":
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
settings = Settings()
settings.configure_logging()
Documenting Required Settings
Create a .env.example file documenting all settings:
# Database configuration
DATABASE_URL=postgresql+asyncpg://user:password@localhost/mydb
DATABASE_POOL_SIZE=10
# Security
SECRET_KEY=generate-a-random-32-character-key-here
ACCESS_TOKEN_EXPIRE_MINUTES=30
# External services
EXTERNAL_API_URL=https://api.example.com
EXTERNAL_API_KEY=your-api-key
# Application
ENVIRONMENT=development
DEBUG=true
LOG_LEVEL=DEBUG
Include instructions on how to generate secrets:
# Generate SECRET_KEY
python -c "import secrets; print(secrets.token_urlsafe(32))"
Key Takeaways
- Use
pydantic-settingsto define configuration as a Pydantic model. - Load settings from
.envfiles, environment variables, and defaults. - Create environment-specific settings classes (development, testing, production).
- Validate critical settings at startup to catch misconfigurations immediately.
- Use secrets managers (AWS Secrets, Vault) in production, not
.envfiles. - Document all settings in
.env.example.
Frequently Asked Questions
Should I commit .env files to version control?
No. Add .env* to .gitignore. Only commit .env.example with placeholders and documentation.
How do I override a single setting for testing?
Use dependency overrides. Define get_settings() as a dependency and override it in tests:
app.dependency_overrides[get_settings] = lambda: Settings(database_url="sqlite:///:memory:")
Can I use environment variables that aren't defined?
If a setting has no default and the environment variable is missing, Pydantic raises a ValidationError at startup. This prevents silent failures.
How do I handle list or dictionary settings?
Use Field with default factories:
from pydantic import Field
class Settings(BaseSettings):
allowed_hosts: list[str] = Field(default_factory=lambda: ["localhost"])
# Or load from JSON: ALLOWED_HOSTS='["example.com", "api.example.com"]'
What's the difference between BaseSettings and BaseModel?
BaseSettings reads from environment variables and .env files; BaseModel does not. Use BaseSettings for app configuration.