Settings Classes¶
Learn how Python settings classes validate, structure, and organize your application configuration. The boilerplate uses Pydantic's BaseSettings
for type-safe configuration management.
Settings Architecture¶
The main Settings
class inherits from multiple specialized setting groups:
# src/app/core/config.py
class Settings(
AppSettings,
PostgresSettings,
CryptSettings,
FirstUserSettings,
RedisCacheSettings,
ClientSideCacheSettings,
RedisQueueSettings,
RedisRateLimiterSettings,
DefaultRateLimitSettings,
EnvironmentSettings,
):
pass
# Single instance used throughout the app
settings = Settings()
Built-in Settings Groups¶
Application Settings¶
Basic app metadata and configuration:
class AppSettings(BaseSettings):
APP_NAME: str = "FastAPI"
APP_DESCRIPTION: str = "A FastAPI project"
APP_VERSION: str = "0.1.0"
CONTACT_NAME: str = "Your Name"
CONTACT_EMAIL: str = "your.email@example.com"
LICENSE_NAME: str = "MIT"
Database Settings¶
PostgreSQL connection configuration:
class PostgresSettings(BaseSettings):
POSTGRES_USER: str
POSTGRES_PASSWORD: str
POSTGRES_SERVER: str = "localhost"
POSTGRES_PORT: int = 5432
POSTGRES_DB: str
@computed_field
@property
def DATABASE_URL(self) -> str:
return (
f"postgresql+asyncpg://{self.POSTGRES_USER}:"
f"{self.POSTGRES_PASSWORD}@{self.POSTGRES_SERVER}:"
f"{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
)
Security Settings¶
JWT and authentication configuration:
class CryptSettings(BaseSettings):
SECRET_KEY: str
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
@field_validator("SECRET_KEY")
@classmethod
def validate_secret_key(cls, v: str) -> str:
if len(v) < 32:
raise ValueError("SECRET_KEY must be at least 32 characters")
return v
Redis Settings¶
Separate Redis instances for different services:
class RedisCacheSettings(BaseSettings):
REDIS_CACHE_HOST: str = "localhost"
REDIS_CACHE_PORT: int = 6379
class RedisQueueSettings(BaseSettings):
REDIS_QUEUE_HOST: str = "localhost"
REDIS_QUEUE_PORT: int = 6379
class RedisRateLimiterSettings(BaseSettings):
REDIS_RATE_LIMIT_HOST: str = "localhost"
REDIS_RATE_LIMIT_PORT: int = 6379
Rate Limiting Settings¶
Default rate limiting configuration:
class DefaultRateLimitSettings(BaseSettings):
DEFAULT_RATE_LIMIT_LIMIT: int = 10
DEFAULT_RATE_LIMIT_PERIOD: int = 3600 # 1 hour
Admin User Settings¶
First superuser account creation:
class FirstUserSettings(BaseSettings):
ADMIN_NAME: str = "Admin"
ADMIN_EMAIL: str
ADMIN_USERNAME: str = "admin"
ADMIN_PASSWORD: str
@field_validator("ADMIN_EMAIL")
@classmethod
def validate_admin_email(cls, v: str) -> str:
if "@" not in v:
raise ValueError("ADMIN_EMAIL must be a valid email")
return v
Creating Custom Settings¶
Basic Custom Settings¶
Add your own settings group:
class CustomSettings(BaseSettings):
CUSTOM_API_KEY: str = ""
CUSTOM_TIMEOUT: int = 30
ENABLE_FEATURE_X: bool = False
MAX_UPLOAD_SIZE: int = 10485760 # 10MB
@field_validator("MAX_UPLOAD_SIZE")
@classmethod
def validate_upload_size(cls, v: int) -> int:
if v < 1024: # 1KB minimum
raise ValueError("MAX_UPLOAD_SIZE must be at least 1KB")
if v > 104857600: # 100MB maximum
raise ValueError("MAX_UPLOAD_SIZE cannot exceed 100MB")
return v
# Add to main Settings class
class Settings(
AppSettings,
PostgresSettings,
# ... other settings ...
CustomSettings, # Add your custom settings
):
pass
Advanced Custom Settings¶
Settings with complex validation and computed fields:
class EmailSettings(BaseSettings):
SMTP_HOST: str = ""
SMTP_PORT: int = 587
SMTP_USERNAME: str = ""
SMTP_PASSWORD: str = ""
SMTP_USE_TLS: bool = True
EMAIL_FROM: str = ""
EMAIL_FROM_NAME: str = ""
@computed_field
@property
def EMAIL_ENABLED(self) -> bool:
return bool(self.SMTP_HOST and self.SMTP_USERNAME)
@model_validator(mode="after")
def validate_email_config(self) -> "EmailSettings":
if self.SMTP_HOST and not self.EMAIL_FROM:
raise ValueError("EMAIL_FROM required when SMTP_HOST is set")
if self.SMTP_USERNAME and not self.SMTP_PASSWORD:
raise ValueError("SMTP_PASSWORD required when SMTP_USERNAME is set")
return self
Feature Flag Settings¶
Organize feature toggles:
class FeatureSettings(BaseSettings):
# Core features
ENABLE_CACHING: bool = True
ENABLE_RATE_LIMITING: bool = True
ENABLE_BACKGROUND_JOBS: bool = True
# Optional features
ENABLE_ANALYTICS: bool = False
ENABLE_EMAIL_NOTIFICATIONS: bool = False
ENABLE_FILE_UPLOADS: bool = False
# Experimental features
ENABLE_EXPERIMENTAL_API: bool = False
ENABLE_BETA_FEATURES: bool = False
@model_validator(mode="after")
def validate_feature_dependencies(self) -> "FeatureSettings":
if self.ENABLE_EMAIL_NOTIFICATIONS and not self.ENABLE_BACKGROUND_JOBS:
raise ValueError("Email notifications require background jobs")
return self
Settings Validation¶
Field Validation¶
Validate individual fields:
class DatabaseSettings(BaseSettings):
DB_POOL_SIZE: int = 20
DB_MAX_OVERFLOW: int = 30
DB_TIMEOUT: int = 30
@field_validator("DB_POOL_SIZE")
@classmethod
def validate_pool_size(cls, v: int) -> int:
if v < 1:
raise ValueError("Pool size must be at least 1")
if v > 100:
raise ValueError("Pool size should not exceed 100")
return v
@field_validator("DB_TIMEOUT")
@classmethod
def validate_timeout(cls, v: int) -> int:
if v < 5:
raise ValueError("Timeout must be at least 5 seconds")
return v
Model Validation¶
Validate across multiple fields:
class SecuritySettings(BaseSettings):
ENABLE_HTTPS: bool = False
SSL_CERT_PATH: str = ""
SSL_KEY_PATH: str = ""
FORCE_SSL: bool = False
@model_validator(mode="after")
def validate_ssl_config(self) -> "SecuritySettings":
if self.ENABLE_HTTPS:
if not self.SSL_CERT_PATH:
raise ValueError("SSL_CERT_PATH required when HTTPS enabled")
if not self.SSL_KEY_PATH:
raise ValueError("SSL_KEY_PATH required when HTTPS enabled")
if self.FORCE_SSL and not self.ENABLE_HTTPS:
raise ValueError("Cannot force SSL without enabling HTTPS")
return self
Environment-Specific Validation¶
Different validation rules per environment:
class EnvironmentSettings(BaseSettings):
ENVIRONMENT: str = "local"
DEBUG: bool = True
@model_validator(mode="after")
def validate_environment_config(self) -> "EnvironmentSettings":
if self.ENVIRONMENT == "production":
if self.DEBUG:
raise ValueError("DEBUG must be False in production")
if self.ENVIRONMENT not in ["local", "staging", "production"]:
raise ValueError("ENVIRONMENT must be local, staging, or production")
return self
Computed Properties¶
Dynamic Configuration¶
Create computed values from other settings:
class StorageSettings(BaseSettings):
STORAGE_TYPE: str = "local" # local, s3, gcs
# Local storage
LOCAL_STORAGE_PATH: str = "./uploads"
# S3 settings
AWS_ACCESS_KEY_ID: str = ""
AWS_SECRET_ACCESS_KEY: str = ""
AWS_BUCKET_NAME: str = ""
AWS_REGION: str = "us-east-1"
@computed_field
@property
def STORAGE_ENABLED(self) -> bool:
if self.STORAGE_TYPE == "local":
return bool(self.LOCAL_STORAGE_PATH)
elif self.STORAGE_TYPE == "s3":
return bool(self.AWS_ACCESS_KEY_ID and self.AWS_SECRET_ACCESS_KEY and self.AWS_BUCKET_NAME)
return False
@computed_field
@property
def STORAGE_CONFIG(self) -> dict:
if self.STORAGE_TYPE == "local":
return {"path": self.LOCAL_STORAGE_PATH}
elif self.STORAGE_TYPE == "s3":
return {
"bucket": self.AWS_BUCKET_NAME,
"region": self.AWS_REGION,
"credentials": {
"access_key": self.AWS_ACCESS_KEY_ID,
"secret_key": self.AWS_SECRET_ACCESS_KEY,
}
}
return {}
Organizing Settings¶
Service-Based Organization¶
Group settings by service or domain:
# Authentication service settings
class AuthSettings(BaseSettings):
JWT_SECRET_KEY: str
JWT_ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE: int = 30
REFRESH_TOKEN_EXPIRE: int = 7200
PASSWORD_MIN_LENGTH: int = 8
# Notification service settings
class NotificationSettings(BaseSettings):
EMAIL_ENABLED: bool = False
SMS_ENABLED: bool = False
PUSH_ENABLED: bool = False
# Email settings
SMTP_HOST: str = ""
SMTP_PORT: int = 587
# SMS settings (example with Twilio)
TWILIO_ACCOUNT_SID: str = ""
TWILIO_AUTH_TOKEN: str = ""
# Main settings
class Settings(
AppSettings,
AuthSettings,
NotificationSettings,
# ... other settings
):
pass
Conditional Settings Loading¶
Load different settings based on environment:
class BaseAppSettings(BaseSettings):
APP_NAME: str = "FastAPI App"
DEBUG: bool = False
class DevelopmentSettings(BaseAppSettings):
DEBUG: bool = True
LOG_LEVEL: str = "DEBUG"
DATABASE_ECHO: bool = True
class ProductionSettings(BaseAppSettings):
DEBUG: bool = False
LOG_LEVEL: str = "WARNING"
DATABASE_ECHO: bool = False
def get_settings() -> BaseAppSettings:
environment = os.getenv("ENVIRONMENT", "local")
if environment == "production":
return ProductionSettings()
else:
return DevelopmentSettings()
settings = get_settings()
Removing Unused Services¶
Minimal Configuration¶
Remove services you don't need:
# Minimal setup without Redis services
class MinimalSettings(
AppSettings,
PostgresSettings,
CryptSettings,
FirstUserSettings,
# Removed: RedisCacheSettings
# Removed: RedisQueueSettings
# Removed: RedisRateLimiterSettings
EnvironmentSettings,
):
pass
Service Feature Flags¶
Use feature flags to conditionally enable services:
class ServiceSettings(BaseSettings):
ENABLE_REDIS: bool = True
ENABLE_CELERY: bool = True
ENABLE_MONITORING: bool = False
class ConditionalSettings(
AppSettings,
PostgresSettings,
CryptSettings,
ServiceSettings,
):
# Add Redis settings only if enabled
def __init__(self, **kwargs):
super().__init__(**kwargs)
if self.ENABLE_REDIS:
# Dynamically add Redis settings
self.__class__ = type(
"ConditionalSettings",
(self.__class__, RedisCacheSettings),
{}
)
Testing Settings¶
Test Configuration¶
Create separate settings for testing:
class TestSettings(BaseSettings):
# Override database for testing
POSTGRES_DB: str = "test_database"
# Disable external services
ENABLE_REDIS: bool = False
ENABLE_EMAIL: bool = False
# Speed up tests
ACCESS_TOKEN_EXPIRE_MINUTES: int = 5
# Test-specific settings
TEST_USER_EMAIL: str = "test@example.com"
TEST_USER_PASSWORD: str = "testpassword123"
# Use in tests
@pytest.fixture
def test_settings():
return TestSettings()
Settings Validation Testing¶
Test your custom settings:
def test_custom_settings_validation():
# Test valid configuration
settings = CustomSettings(
CUSTOM_API_KEY="test-key",
CUSTOM_TIMEOUT=60,
MAX_UPLOAD_SIZE=5242880 # 5MB
)
assert settings.CUSTOM_TIMEOUT == 60
# Test validation error
with pytest.raises(ValueError, match="MAX_UPLOAD_SIZE cannot exceed 100MB"):
CustomSettings(MAX_UPLOAD_SIZE=209715200) # 200MB
def test_settings_computed_fields():
settings = StorageSettings(
STORAGE_TYPE="s3",
AWS_ACCESS_KEY_ID="test-key",
AWS_SECRET_ACCESS_KEY="test-secret",
AWS_BUCKET_NAME="test-bucket"
)
assert settings.STORAGE_ENABLED is True
assert settings.STORAGE_CONFIG["bucket"] == "test-bucket"
Best Practices¶
Organization¶
- Group related settings in dedicated classes
- Use descriptive names for settings groups
- Keep validation logic close to the settings
- Document complex validation rules
Security¶
- Validate sensitive settings like secret keys
- Never set default values for secrets in production
- Use computed fields to derive connection strings
- Separate test and production configurations
Performance¶
- Use
@computed_field
for expensive calculations - Cache settings instances appropriately
- Avoid complex validation in hot paths
- Use model validators for cross-field validation
Testing¶
- Create separate test settings classes
- Test all validation rules
- Mock external service settings in tests
- Use dependency injection for settings in tests
The settings system provides type safety, validation, and organization for your application configuration. Start with the built-in settings and extend them as your application grows!