Skip to content

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!