🎓 What You Will Learn
- Configuration Management: Why hardcoding settings is dangerous
- Pydantic Settings: Using BaseSettings for configuration
- Environment Variables: Loading from .env files
- Validation: Type checking and validation of settings
- Secrets: Managing sensitive data securely
- Multiple Environments: Development, staging, production configs
1Why Configuration Management Matters
Hardcoding configuration (database URLs, API keys, credentials) is dangerous. It exposes secrets and makes it impossible to run the same code in different environments. Pydantic Settings provides a clean, type-safe way to manage configuration.
Configuration should never be hardcoded. Different environments (development, staging, production) need different settings. Use environment variables to inject configuration at runtime.
❌ Hardcoded (Bad)
2Pydantic Settings Basics
Pydantic Settings extends Pydantic to load configuration from environment variables and .env files. Install it first.
pip install pydantic-settingsfrom pydantic_settings import BaseSettings
class Settings(BaseSettings):
# Required settings (no default value)
database_url: str
api_key: str
# Optional settings with defaults
debug: bool = False
log_level: str = "INFO"
port: int = 8000
class Config:
env_file = ".env" # Load from .env file
# Create global settings instance
settings = Settings()
3Loading from .env Files
Create a .env file in your project root with environment variables. Pydantic Settings loads them automatically.
DATABASE_URL=postgresql://user:password@localhost/mydb
API_KEY=sk_live_1234567890
DEBUG=false
LOG_LEVEL=INFO
PORT=8000
SECRET_KEY=your-secret-key-here
Security Warning: Never commit .env to version control. Add it to .gitignore.
# Ignore environment files
.env
.env.local
.env.*.local
.env.prod
# Ignore IDE
.vscode/
.idea/
# Ignore Python
__pycache__/
*.pyc
.venv/
venv/
4Type Validation & Defaults
Pydantic validates types automatically. If you set port: int = 8000, Pydantic ensures the value is an integer.
from pydantic_settings import BaseSettings
from typing import Optional
class Settings(BaseSettings):
# Required string
database_url: str
# Optional string
redis_url: Optional[str] = None
# Integer with default
port: int = 8000
# Boolean with default
debug: bool = False
# List of strings
allowed_hosts: list[str] = ["localhost", "127.0.0.1"]
class Config:
env_file = ".env"
settings = Settings()
# Usage
print(settings.port) # 8000 (int)
print(settings.debug) # False (bool)
print(settings.allowed_hosts) # ["localhost", "127.0.0.1"] (list)
Type Hints: Use type hints for automatic validation. Optional[str] allows None, str requires a value.
5Integrating with FastAPI
Create a settings module and use it throughout your FastAPI application.
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "My API"
database_url: str
api_key: str
debug: bool = False
log_level: str = "INFO"
class Config:
env_file = ".env"
case_sensitive = False
settings = Settings()
from fastapi import FastAPI
from app.config import settings
from sqlalchemy import create_engine
# Use settings in your FastAPI app
app = FastAPI(title=settings.app_name, debug=settings.debug)
# Create database engine with configured URL
engine = create_engine(settings.database_url)
@app.get("/")
async def root():
return {
"app_name": settings.app_name,
"debug": settings.debug,
"log_level": settings.log_level
}
@app.get("/config")
async def get_config():
return {
"database_url": settings.database_url[:20] + "...", # Hide password
"api_key": "hidden",
"debug": settings.debug
}
6Environment-Specific Settings
Support multiple environments (development, staging, production) by using environment variables to control behavior.
import os
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
environment: str = os.getenv("ENV", "development")
database_url: str
api_key: str
# Development vs Production
debug: bool = environment == "development"
log_level: str = "DEBUG" if environment == "development" else "INFO"
# Database settings
db_pool_size: int = 5
db_max_overflow: int = 10
# Security
cors_origins: list[str] = ["http://localhost:3000"]
class Config:
env_file = ".env"
def __init__(self, **data):
super().__init__(**data)
# Load environment-specific .env file
if self.environment == "production":
self.cors_origins = ["https://example.com", "https://www.example.com"]
self.db_pool_size = 20
settings = Settings()
ENV=development
DATABASE_URL=postgresql://user:password@localhost/mydb_dev
API_KEY=dev_key_12345
DEBUG=true
LOG_LEVEL=DEBUG
CORS_ORIGINS=["http://localhost:3000", "http://localhost:3001"]
ENV=production
DATABASE_URL=postgresql://user:[email protected]/mydb
API_KEY=sk_live_actual_key_here
DEBUG=false
LOG_LEVEL=INFO
CORS_ORIGINS=["https://example.com", "https://www.example.com"]
7Custom Validation
Validate settings using field validators or custom logic.
from pydantic_settings import BaseSettings
from pydantic import field_validator
class Settings(BaseSettings):
database_url: str
api_key: str
port: int = 8000
class Config:
env_file = ".env"
@field_validator("database_url")
@classmethod
def validate_database_url(cls, v):
if not v.startswith(("postgresql://", "mysql://", "sqlite://")):
raise ValueError("Invalid database URL format")
return v
@field_validator("api_key")
@classmethod
def validate_api_key(cls, v):
if len(v) < 20:
raise ValueError("API key must be at least 20 characters")
return v
@field_validator("port")
@classmethod
def validate_port(cls, v):
if not 1 <= v <= 65535:
raise ValueError("Port must be between 1 and 65535")
return v
settings = Settings()
8Secrets Management with .env
For sensitive data, use .env files that are never committed to git. In production, use environment variables or secret management systems.
from pydantic_settings import BaseSettings
from typing import Optional
class Settings(BaseSettings):
# Secrets (from .env or environment)
database_password: str
api_key: str
jwt_secret: str
aws_access_key: Optional[str] = None
aws_secret_key: Optional[str] = None
# Non-secrets (can be in git)
app_name: str = "My API"
database_host: str = "localhost"
debug: bool = False
class Config:
env_file = ".env"
# Warn if env_file is missing
case_sensitive = True
settings = Settings()
Production Secrets: Use AWS Secrets Manager, Azure Key Vault, HashiCorp Vault, or similar for production. Never rely on .env files in production.
9Nested Configuration Objects
For complex configurations, use nested Pydantic models to organize settings.
from pydantic_settings import BaseSettings
from pydantic import BaseModel
from typing import Optional
class DatabaseConfig(BaseModel):
url: str
pool_size: int = 5
max_overflow: int = 10
echo: bool = False
class CORSConfig(BaseModel):
origins: list[str] = ["localhost"]
allow_credentials: bool = True
allow_methods: list[str] = ["*"]
allow_headers: list[str] = ["*"]
class Settings(BaseSettings):
app_name: str = "My API"
debug: bool = False
database: DatabaseConfig
cors: CORSConfig
class Config:
env_file = ".env"
env_nested_delimiter = "__"
# .env file
# DATABASE__URL=postgresql://user:pass@localhost/db
# DATABASE__POOL_SIZE=10
# CORS__ORIGINS=["https://example.com"]
# CORS__ALLOW_CREDENTIALS=true
settings = Settings()
print(settings.database.url) # postgresql://user:pass@localhost/db
print(settings.cors.origins) # ["https://example.com"]
10Testing with Different Settings
Override settings in tests to avoid using production credentials.
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.config import settings
@pytest.fixture
def test_client():
# Create test client with test settings
with TestClient(app) as client:
yield client
def test_app_with_test_settings(monkeypatch):
# Override settings for this test
monkeypatch.setattr(settings, "debug", True)
monkeypatch.setattr(settings, "database_url", "sqlite:///:memory:")
assert settings.debug is True
assert "sqlite" in settings.database_url
def test_config_loaded():
# Test that settings loaded correctly
assert settings.app_name is not None
assert settings.database_url is not None
assert settings.port > 0
11Common Configuration Patterns
| Pattern | Use Case | Example |
|---|---|---|
| Environment-based config | Different settings per environment | ENV=production DATABASE_URL=... |
| Nested models | Organize complex configurations | database.url, cors.origins |
| Custom validators | Validate sensitive settings | Ensure API key format |
| Optional secrets | Some features optional | AWS_KEY optional in development |
| Derived settings | Calculate from other settings | debug = (environment == development) |
12Configuration Best Practices
- Never commit .env to version control
- Use type hints for all settings
- Set reasonable defaults for optional settings
- Validate sensitive settings (API keys, URLs)
- Use environment-specific .env files (.env.production, .env.development)
- Hide sensitive data when logging settings
- Use secret management systems in production
- Document all required settings in README
- Test with different configurations
- Override settings in tests to avoid side effects
13Production Configuration Setup
In production, load settings from environment variables or secret management services.
import os
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "Production API"
environment: str = os.getenv("ENV", "production")
database_url: str # Required in production
api_key: str # Required in production
jwt_secret: str # Required in production
aws_access_key: str = os.getenv("AWS_ACCESS_KEY_ID", "")
log_level: str = "INFO"
class Config:
env_file = ".env" # Will be empty in production
case_sensitive = True
def __post_init__(self):
# Validate required settings in production
if self.environment == "production":
required = ["database_url", "api_key", "jwt_secret"]
for field in required:
if not getattr(self, field):
raise ValueError(f"{field} must be set in production")
settings = Settings()
Docker & Environment Variables: In Docker/Kubernetes, pass secrets as environment variables or use a secret manager. The application loads them at startup.
14Advanced Configuration Patterns
from pydantic_settings import BaseSettings
from functools import lru_cache
class Settings(BaseSettings):
database_url: str
api_key: str
debug: bool = False
class Config:
env_file = ".env"
# Cache settings instance
@lru_cache()
def get_settings():
return Settings()
# Usage in FastAPI
from fastapi import Depends
async def some_endpoint(settings: Settings = Depends(get_settings)):
return {"debug": settings.debug}
15Summary & Next Steps
Master Pydantic Settings to build secure, flexible FastAPI applications that work across development, staging, and production environments.
🚀 Congratulations! You now understand how to manage configuration securely in FastAPI using Pydantic Settings. Build with confidence across all environments!