Configuration Management

Flexible and Secure Configuration for Flask Applications

Introduction to Configuration Management

Configuration management is a critical aspect of building robust and maintainable web applications. As your Flask application grows, you'll need to manage various settings, from database connections to API keys, across different environments like development, testing, and production.

Proper configuration management helps you:

Think of configuration as the control panel for your application - it lets you adjust how your application behaves without changing the code itself.

graph LR A[Configuration Sources] --> B[Flask Application] B --> C[Development Environment] B --> D[Testing Environment] B --> E[Production Environment] A --> A1[Environment Variables] A --> A2[Configuration Files] A --> A3[Secret Management] A --> A4[Default Values] style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#bbf,stroke:#333,stroke-width:2px style C fill:#bfb,stroke:#333,stroke-width:1px style D fill:#fbf,stroke:#333,stroke-width:1px style E fill:#fbb,stroke:#333,stroke-width:1px

Flask Configuration Basics

Flask provides a simple way to manage configuration through the app.config dictionary-like object. This object behaves like a dictionary and is used throughout your application to access configuration values.

Setting Configuration Values

# Direct assignment
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['DEBUG'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'

# Update multiple values at once
app.config.update(
    TESTING=True,
    SECRET_KEY='test-key',
    SQLALCHEMY_DATABASE_URI='sqlite:///:memory:'
)

Accessing Configuration Values

# Direct access from app.config
secret_key = app.config['SECRET_KEY']
debug_mode = app.config['DEBUG']

# Using get() with defaults
secret_key = app.config.get('SECRET_KEY', 'default-secret-key')
debug_mode = app.config.get('DEBUG', False)

Built-in Configuration Values

Flask has several built-in configuration values that control its behavior:

Configuration Key Description Default
DEBUG Enable/disable debug mode False
TESTING Enable/disable testing mode False
SECRET_KEY Secret key for signing cookies, etc. None
SERVER_NAME The host and port for the server None
APPLICATION_ROOT Path where application is mounted '/'
PREFERRED_URL_SCHEME URL scheme to use (http or https) 'http'
TEMPLATES_AUTO_RELOAD Reload templates when they change None (True in debug mode)
EXPLAIN_TEMPLATE_LOADING Log how templates are found False
MAX_CONTENT_LENGTH Maximum request content length None
SEND_FILE_MAX_AGE_DEFAULT Default cache timeout for static files 43200 (12 hours)

Extensions like Flask-SQLAlchemy, Flask-Mail, or Flask-WTF also have their own configuration values, typically prefixed with their name.

Configuration Loading Methods

Flask provides several methods to load configuration from different sources.

Loading from Python Files

# config.py
SECRET_KEY = 'your-secret-key'
DEBUG = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///app.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False

# app.py
app.config.from_pyfile('config.py')

This approach lets you store configuration in a separate Python file, which can be excluded from version control if it contains sensitive information.

Loading from Objects

# config.py
class Config:
    SECRET_KEY = 'your-secret-key'
    DEBUG = False
    SQLALCHEMY_DATABASE_URI = 'sqlite:///app.db'
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    
class DevelopmentConfig(Config):
    DEBUG = True
    
class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
    
class ProductionConfig(Config):
    SECRET_KEY = 'hard-to-guess-secret'
    SQLALCHEMY_DATABASE_URI = 'postgresql://user:password@localhost/app'

# app.py
app.config.from_object('config.DevelopmentConfig')

Using objects allows for inheritance and organization of configuration for different environments. You can choose which configuration to use based on an environment variable:

# Set the environment variable
# export FLASK_ENV=production

# app.py
import os
from flask import Flask

app = Flask(__name__)
app_env = os.environ.get('FLASK_ENV', 'development')

if app_env == 'production':
    app.config.from_object('config.ProductionConfig')
elif app_env == 'testing':
    app.config.from_object('config.TestingConfig')
else:
    app.config.from_object('config.DevelopmentConfig')

Loading from JSON Files

# config.json
{
    "SECRET_KEY": "your-secret-key",
    "DEBUG": true,
    "SQLALCHEMY_DATABASE_URI": "sqlite:///app.db",
    "SQLALCHEMY_TRACK_MODIFICATIONS": false
}

# app.py
app.config.from_json('config.json')

JSON files can be useful for configuration that needs to be read by other tools or languages, not just Python.

Loading from Environment Variables

# app.py
app.config.from_envvar('APP_CONFIG_FILE')

# Then run the application with:
# export APP_CONFIG_FILE=/path/to/config.py
# flask run

This approach loads a configuration file specified by an environment variable, allowing for easy switching between configurations during deployment.

You can also load specific values from environment variables:

# app.py
import os

app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-key')
app.config['DATABASE_URL'] = os.environ.get('DATABASE_URL', 'sqlite:///app.db')

Configuration Patterns in the Application Factory

When using the application factory pattern, configuration is typically loaded inside the factory function. This approach allows for different configurations to be used when creating application instances.

Basic Configuration in Factory

# app/__init__.py
from flask import Flask

def create_app(config_name=None):
    app = Flask(__name__)
    
    # Default configuration
    app.config.from_mapping(
        SECRET_KEY='dev',
        SQLALCHEMY_DATABASE_URI='sqlite:///app.db',
        SQLALCHEMY_TRACK_MODIFICATIONS=False,
    )
    
    # Override with custom configuration
    if config_name == 'production':
        app.config.from_object('config.ProductionConfig')
    elif config_name == 'testing':
        app.config.from_object('config.TestingConfig')
    else:  # development
        app.config.from_object('config.DevelopmentConfig')
    
    # ... register blueprints, extensions, etc. ...
    
    return app

Environment-based Configuration

# app/__init__.py
import os
from flask import Flask

def create_app():
    app = Flask(__name__)
    
    # Load default configuration
    app.config.from_mapping(
        SECRET_KEY='dev',
        SQLALCHEMY_DATABASE_URI='sqlite:///app.db',
        SQLALCHEMY_TRACK_MODIFICATIONS=False,
    )
    
    # Determine environment
    env = os.environ.get('FLASK_ENV', 'development')
    
    # Load environment-specific configuration
    if env == 'production':
        app.config.from_object('config.ProductionConfig')
    elif env == 'testing':
        app.config.from_object('config.TestingConfig')
    else:
        app.config.from_object('config.DevelopmentConfig')
    
    # Optional: Load environment variables from .env file
    from dotenv import load_dotenv
    load_dotenv()
    
    # Override with environment variables
    for key in ['SECRET_KEY', 'DATABASE_URL', 'MAIL_SERVER', 'MAIL_PORT']:
        if key in os.environ:
            app.config[key] = os.environ[key]
    
    # ... register blueprints, extensions, etc. ...
    
    return app

Configuration with Instance Folders

Flask provides an "instance folder" concept for configuration that shouldn't be committed to version control:

# Create the application with an instance folder
app = Flask(__name__, instance_relative_config=True)

# Load default configuration
app.config.from_object('config.default')

# Load instance configuration, if it exists
app.config.from_pyfile('config.py', silent=True)

The instance folder is located outside the application package and is not included in version control. This is useful for storing sensitive information like API keys or database credentials.

Directory structure:

/myapp
  /app
    __init__.py
    views.py
    /templates
    /static
  /instance
    config.py        # Instance-specific config, not in version control
  /tests
  config.py          # Default config, in version control
  requirements.txt
  run.py

Managing Environment Variables

Environment variables are a secure way to manage configuration, especially in production. They keep sensitive data out of your code repository and allow for easy configuration changes without code modifications.

Using python-dotenv

The python-dotenv package makes it easy to work with environment variables during development:

# Install python-dotenv
pip install python-dotenv

# .env file (NOT checked into version control)
SECRET_KEY=your-secret-key
DATABASE_URL=postgresql://user:password@localhost/app
MAIL_SERVER=smtp.example.com
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_USERNAME=your-email@example.com
MAIL_PASSWORD=your-email-password

Then load environment variables in your application:

from dotenv import load_dotenv
import os

# Load environment variables from .env file
load_dotenv()

# Access environment variables
secret_key = os.environ.get('SECRET_KEY')
database_url = os.environ.get('DATABASE_URL')

This approach is especially useful during development, as it allows you to store configuration in a file rather than setting environment variables manually.

Environment Variables in Production

In production, environment variables are typically set at the system level or through your deployment platform:

# Setting environment variables in Linux/macOS
export SECRET_KEY=your-production-secret-key
export DATABASE_URL=postgresql://user:password@database.example.com/app

# Setting environment variables in Windows
set SECRET_KEY=your-production-secret-key
set DATABASE_URL=postgresql://user:password@database.example.com/app

Cloud platforms like Heroku, AWS Elastic Beanstalk, or Google App Engine provide interfaces to set environment variables for your application.

graph TD A[Environment Variables] --> B[Local Development] A --> C[CI/CD Pipeline] A --> D[Production Environment] B --> B1[.env file] C --> C1[CI/CD Configuration] D --> D1[Cloud Platform Settings] D --> D2[Container Environment] D --> D3[Server Configuration] style A fill:#f9f,stroke:#333,stroke-width:2px

Secrets Management

Proper secrets management is crucial for application security. Secrets include API keys, database credentials, encryption keys, and other sensitive data.

Best Practices for Secrets

  1. Never Store Secrets in Version Control: Even if the repository is private, it's not secure for sensitive data.
  2. Use Environment Variables: Store secrets as environment variables, not in configuration files.
  3. Provide Default Values for Development: Use non-sensitive defaults for development, but don't rely on them for production.
  4. Rotate Secrets Regularly: Change sensitive keys and passwords periodically, especially after team member changes.
  5. Limit Access to Secrets: Only grant access to people who really need it.

Handling Secret Files

When using .env files or similar for development, ensure they're properly excluded from version control:

# .gitignore
.env
.flaskenv
instance/
*.pem
*.key

Provide templates for configuration files to guide developers:

# .env.example (checked into version control)
# Copy this file to .env and fill in the values
SECRET_KEY=your-secret-key
DATABASE_URL=postgresql://user:password@localhost/app
MAIL_SERVER=smtp.example.com
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_USERNAME=your-email@example.com
MAIL_PASSWORD=your-email-password

Secret Management Services

For larger applications or teams, consider using dedicated secret management services:

These services provide secure storage, access control, and rotation of secrets, as well as integration with your application environment.

Type-Safe Configuration

Flask's configuration system is dictionary-based, which means it doesn't provide type checking or validation by default. For more robust configuration, you can implement type-safe configuration using Python's type annotations and validation libraries.

Using Pydantic for Configuration

Pydantic is a powerful data validation library that works well for configuration:

from pydantic import BaseSettings, Field, validator
from typing import Optional
import os
from dotenv import load_dotenv

load_dotenv()

class AppSettings(BaseSettings):
    # Flask settings
    SECRET_KEY: str = Field(..., min_length=16)
    DEBUG: bool = False
    TESTING: bool = False
    SERVER_NAME: Optional[str] = None
    
    # Database settings
    DATABASE_URL: str
    SQLALCHEMY_TRACK_MODIFICATIONS: bool = False
    
    # Email settings
    MAIL_SERVER: str
    MAIL_PORT: int
    MAIL_USE_TLS: bool = True
    MAIL_USERNAME: str
    MAIL_PASSWORD: str
    
    # Custom validation
    @validator('DATABASE_URL')
    def validate_database_url(cls, v):
        if not v.startswith(('sqlite:///', 'postgresql://', 'mysql://')):
            raise ValueError('Invalid database URL scheme')
        return v
    
    class Config:
        env_file = '.env'
        case_sensitive = True

# Usage
settings = AppSettings()

def create_app():
    app = Flask(__name__)
    
    # Use Pydantic settings to configure Flask
    app.config.update(settings.dict())
    
    # ... register blueprints, extensions, etc. ...
    
    return app

This approach provides several benefits:

Using dataclasses

For a lighter-weight approach, you can use Python's built-in dataclasses:

from dataclasses import dataclass
from typing import Optional
import os

@dataclass
class Config:
    SECRET_KEY: str = os.environ.get('SECRET_KEY', 'dev-key')
    DEBUG: bool = os.environ.get('DEBUG', 'False').lower() == 'true'
    TESTING: bool = False
    SQLALCHEMY_DATABASE_URI: str = os.environ.get('DATABASE_URL', 'sqlite:///app.db')
    SQLALCHEMY_TRACK_MODIFICATIONS: bool = False
    MAIL_SERVER: Optional[str] = os.environ.get('MAIL_SERVER')
    MAIL_PORT: int = int(os.environ.get('MAIL_PORT', 0)) or None
    
    def to_dict(self):
        return {key: value for key, value in self.__dict__.items()
                if not key.startswith('_')}
def create_app():
    app = Flask(__name__)
    
    # Use dataclass config
    config = Config()
    app.config.update(config.to_dict())
    
    # ... register blueprints, extensions, etc. ...
    
    return app

Environment-specific Configuration

Different environments (development, testing, production) often require different configurations. Let's explore patterns for managing these environments.

Using Configuration Classes

A common pattern is to use inheritance with configuration classes:

# config.py
import os

class Config:
    """Base configuration."""
    SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-key')
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    
    # Mail settings
    MAIL_SERVER = os.environ.get('MAIL_SERVER', 'smtp.example.com')
    MAIL_PORT = int(os.environ.get('MAIL_PORT', 587))
    MAIL_USE_TLS = True
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
    
    # Application settings
    ITEMS_PER_PAGE = 10
    
    @staticmethod
    def init_app(app):
        """Initialize application with this configuration."""
        pass

class DevelopmentConfig(Config):
    """Development configuration."""
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL',
                                           'sqlite:///dev.db')
    
    @classmethod
    def init_app(cls, app):
        super(DevelopmentConfig, cls).init_app(app)
        app.logger.setLevel('DEBUG')

class TestingConfig(Config):
    """Testing configuration."""
    TESTING = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL',
                                           'sqlite:///:memory:')
    WTF_CSRF_ENABLED = False
    
    @classmethod
    def init_app(cls, app):
        super(TestingConfig, cls).init_app(app)
        # Disable error emails during testing
        app.config['MAIL_SUPPRESS_SEND'] = True
    
class ProductionConfig(Config):
    """Production configuration."""
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
    
    # Security settings
    SESSION_COOKIE_SECURE = True
    REMEMBER_COOKIE_SECURE = True
    
    @classmethod
    def init_app(cls, app):
        super(ProductionConfig, cls).init_app(app)
        
        # Email errors to administrators
        import logging
        from logging.handlers import SMTPHandler
        
        mail_handler = SMTPHandler(
            mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
            fromaddr=app.config['MAIL_USERNAME'],
            toaddrs=['admin@example.com'],
            subject='Application Error',
            credentials=(app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD']),
            secure=()
        )
        mail_handler.setLevel(logging.ERROR)
        app.logger.addHandler(mail_handler)

# Dictionary with configuration objects
config = {
    'development': DevelopmentConfig,
    'testing': TestingConfig,
    'production': ProductionConfig,
    'default': DevelopmentConfig
}

Then in your application factory:

def create_app(config_name=None):
    app = Flask(__name__)
    
    # Determine configuration to use
    config_name = config_name or os.environ.get('FLASK_ENV', 'default')
    app.config.from_object(config[config_name])
    
    # Call init_app method to perform additional setup
    config[config_name].init_app(app)
    
    # ... register blueprints, extensions, etc. ...
    
    return app

This approach provides several benefits:

Accessing Configuration Outside Request Context

In Flask, configuration is typically accessed via the app.config dictionary. However, sometimes you need to access configuration outside of a request context, such as in background tasks or scripts.

Using current_app

Within a request context, you can use current_app to access the application's configuration:

from flask import current_app

def some_function():
    secret_key = current_app.config['SECRET_KEY']
    debug = current_app.config['DEBUG']
    return f"Secret key: {secret_key}, Debug: {debug}"

Outside a request context, you need to create an application context:

from myapp import create_app
from flask import current_app

app = create_app()

with app.app_context():
    # Now current_app is available
    secret_key = current_app.config['SECRET_KEY']
    debug = current_app.config['DEBUG']

Using Flask-AppConfig

For more complex applications, you might want to use a dedicated extension like Flask-AppConfig to manage configuration outside the Flask application:

from flask_appconfig import AppConfig

def create_app():
    app = Flask(__name__)
    
    # Initialize AppConfig
    AppConfig(app)
    
    # ... register blueprints, extensions, etc. ...
    
    return app

Singleton Configuration

Another approach is to create a singleton configuration object that can be imported anywhere:

# config.py
import os
from dotenv import load_dotenv

load_dotenv()

class AppConfig:
    """Singleton configuration class."""
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(AppConfig, cls).__new__(cls)
            cls._instance._load_config()
        return cls._instance
    
    def _load_config(self):
        """Load configuration from environment variables."""
        self.SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-key')
        self.DEBUG = os.environ.get('DEBUG', 'False').lower() == 'true'
        self.SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', 'sqlite:///app.db')
        # ... more configuration ...
    
    def to_flask_config(self):
        """Convert to dictionary for Flask config."""
        return {key: value for key, value in self.__dict__.items()
                if not key.startswith('_')}

# Import and use anywhere
from config import AppConfig
config = AppConfig()

# In app factory
def create_app():
    app = Flask(__name__)
    app.config.update(config.to_flask_config())
    return app

# In utility modules
def some_utility():
    from config import AppConfig
    config = AppConfig()
    return f"Using database: {config.SQLALCHEMY_DATABASE_URI}"

This approach allows you to access configuration from anywhere in your application, not just within Flask request contexts.

Testing with Different Configurations

Testing often requires specific configurations, such as using an in-memory database or disabling features that would make testing difficult.

Test-specific Configuration

# tests/conftest.py
import pytest
from myapp import create_app
from myapp.extensions import db

@pytest.fixture
def app():
    """Create application for testing."""
    app = create_app('testing')
    
    with app.app_context():
        db.create_all()
        yield app
        db.drop_all()

@pytest.fixture
def client(app):
    """Create test client."""
    return app.test_client()

@pytest.fixture
def runner(app):
    """Create CLI test runner."""
    return app.test_cli_runner()

This approach allows you to use a testing-specific configuration that might:

Overriding Configuration in Tests

Sometimes you need to override specific configuration values for individual tests:

def test_with_custom_config(app, client):
    """Test with a custom configuration value."""
    app.config['ITEMS_PER_PAGE'] = 5  # Override for this test
    
    response = client.get('/items')
    # Assert that only 5 items are displayed
    assert len(response.json['items']) == 5

You can also create a fixture for specific configuration scenarios:

@pytest.fixture
def app_with_mail_disabled():
    """Create application with mail disabled."""
    app = create_app('testing')
    app.config['MAIL_SUPPRESS_SEND'] = True
    return app

def test_password_reset(app_with_mail_disabled, client):
    """Test password reset without sending emails."""
    # Test password reset functionality
    # ...
    # No actual emails will be sent due to MAIL_SUPPRESS_SEND

Configuration Validation

Validating configuration at startup can prevent runtime errors and provide clear error messages when configuration is missing or invalid.

Basic Validation

def create_app():
    app = Flask(__name__)
    app.config.from_object('config.ProductionConfig')
    
    # Validate required configuration
    required_config = ['SECRET_KEY', 'SQLALCHEMY_DATABASE_URI', 'MAIL_SERVER']
    for config_key in required_config:
        if not app.config.get(config_key):
            raise ValueError(f"Missing required configuration: {config_key}")
    
    # Validate specific values
    if app.config['SECRET_KEY'] == 'default-secret-key':
        app.logger.warning("Using default SECRET_KEY in production is not secure!")
    
    # ... register blueprints, extensions, etc. ...
    
    return app

Advanced Validation

For more complex validation, you can define validation functions:

def validate_database_url(url):
    """Validate database URL format."""
    valid_schemes = ('sqlite', 'postgresql', 'mysql')
    if not url:
        return False
    
    try:
        from sqlalchemy.engine.url import make_url
        parsed = make_url(url)
        return parsed.drivername.split('+')[0] in valid_schemes
    except Exception:
        return False

def validate_mail_config(config):
    """Validate mail configuration."""
    if not config.get('MAIL_SERVER'):
        return False
    
    if config.get('MAIL_USE_TLS') or config.get('MAIL_USE_SSL'):
        return bool(config.get('MAIL_PORT'))
    
    return True

def create_app():
    app = Flask(__name__)
    app.config.from_object('config.ProductionConfig')
    
    # Validate database configuration
    db_url = app.config.get('SQLALCHEMY_DATABASE_URI')
    if not validate_database_url(db_url):
        raise ValueError(f"Invalid database URL: {db_url}")
    
    # Validate mail configuration if mail is enabled
    if app.config.get('MAIL_ENABLED', True) and not validate_mail_config(app.config):
        raise ValueError("Invalid mail configuration")
    
    # ... register blueprints, extensions, etc. ...
    
    return app

Validation ensures that your application fails fast with clear error messages when configuration is incorrect, rather than failing in unexpected ways during runtime.

Real-world Configuration Example

Let's look at a comprehensive configuration setup for a real-world Flask application:

graph TB A[Application Factory] --> B[Base Config] B --> C[Development Config] B --> D[Testing Config] B --> E[Production Config] E --> F[Staging Config] A -->|Loads| G[Environment Variables] A -->|Loads| H[Instance Config] G -->|Provides| I[Sensitive Data] H -->|Provides| J[Local Overrides] A -->|Validates| K[Configuration Validator] style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#bbf,stroke:#333,stroke-width:1px style K fill:#fbf,stroke:#333,stroke-width:1px

Configuration Module (config.py)

import os
import secrets
from datetime import timedelta

class Config:
    """Base configuration."""
    # Flask
    SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-key')
    SESSION_COOKIE_NAME = 'my_app_session'
    
    # Security
    SESSION_COOKIE_SECURE = False
    REMEMBER_COOKIE_SECURE = False
    REMEMBER_COOKIE_DURATION = timedelta(days=14)
    
    # Database
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    
    # Mail
    MAIL_SERVER = os.environ.get('MAIL_SERVER', 'smtp.example.com')
    MAIL_PORT = int(os.environ.get('MAIL_PORT', 587))
    MAIL_USE_TLS = True
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
    MAIL_DEFAULT_SENDER = os.environ.get('MAIL_DEFAULT_SENDER', 'noreply@example.com')
    
    # Application
    ITEMS_PER_PAGE = 20
    UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'uploads')
    MAX_CONTENT_LENGTH = 16 * 1024 * 1024  # 16 MB
    
    # Redis (for caching and task queue)
    REDIS_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379/0')
    
    # Celery (for background tasks)
    CELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL', REDIS_URL)
    CELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND', REDIS_URL)
    
    @classmethod
    def init_app(cls, app):
        """Initialize application with this configuration."""
        # Create upload folder if it doesn't exist
        os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)

class DevelopmentConfig(Config):
    """Development configuration."""
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL',
                                           'sqlite:///dev.db')
    
    # Development-specific settings
    TEMPLATES_AUTO_RELOAD = True
    SEND_FILE_MAX_AGE_DEFAULT = 0  # Disable caching for static files
    
    @classmethod
    def init_app(cls, app):
        super(DevelopmentConfig, cls).init_app(app)
        app.logger.setLevel('DEBUG')

class TestingConfig(Config):
    """Testing configuration."""
    TESTING = True
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL',
                                           'sqlite:///:memory:')
    WTF_CSRF_ENABLED = False
    MAIL_SUPPRESS_SEND = True
    SERVER_NAME = 'localhost.localdomain'
    
    @classmethod
    def init_app(cls, app):
        super(TestingConfig, cls).init_app(app)
        # Use simple password hashing for faster tests
        app.config['BCRYPT_LOG_ROUNDS'] = 4

class ProductionConfig(Config):
    """Production configuration."""
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
    
    # Security settings
    SESSION_COOKIE_SECURE = True
    REMEMBER_COOKIE_SECURE = True
    
    # Generate a strong random key if not provided
    SECRET_KEY = os.environ.get('SECRET_KEY') or secrets.token_hex(32)
    
    @classmethod
    def init_app(cls, app):
        super(ProductionConfig, cls).init_app(app)
        
        # Log to syslog
        import logging
        from logging.handlers import SysLogHandler
        syslog_handler = SysLogHandler()
        syslog_handler.setLevel(logging.WARNING)
        app.logger.addHandler(syslog_handler)

class StagingConfig(ProductionConfig):
    """Staging configuration (inherits from production)."""
    
    @classmethod
    def init_app(cls, app):
        super(StagingConfig, cls).init_app(app)
        # Add staging-specific initialization

# Dictionary with configuration objects
config = {
    'development': DevelopmentConfig,
    'testing': TestingConfig,
    'production': ProductionConfig,
    'staging': StagingConfig,
    'default': DevelopmentConfig
}

Application Factory with Configuration Loading

# app/__init__.py
import os
from flask import Flask
from config import config
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv()

def create_app(config_name=None):
    """Create and configure the Flask application."""
    app = Flask(__name__, instance_relative_config=True)
    
    # Determine configuration to use
    config_name = config_name or os.environ.get('FLASK_ENV', 'default')
    
    # Load configuration from config object
    app.config.from_object(config[config_name])
    
    # Load instance configuration, if it exists (overrides config objects)
    app.config.from_pyfile('config.py', silent=True)
    
    # Call init_app method to perform additional setup
    config[config_name].init_app(app)
    
    # Validate critical configuration
    validate_configuration(app)
    
    # Register extensions
    register_extensions(app)
    
    # Register blueprints
    register_blueprints(app)
    
    # Register error handlers
    register_errorhandlers(app)
    
    # Register shell context
    register_shellcontext(app)
    
    # Register CLI commands
    register_commands(app)
    
    return app

def validate_configuration(app):
    """Validate critical configuration values."""
    # Ensure secret key is set and not default
    if app.config['SECRET_KEY'] == 'dev-key' and not app.debug:
        app.logger.warning("Using default SECRET_KEY in production is not secure!")
    
    # Ensure database URL is set
    if not app.config.get('SQLALCHEMY_DATABASE_URI'):
        raise ValueError("SQLALCHEMY_DATABASE_URI must be set!")
    
    # Additional validation for specific environments
    if not app.debug and not app.config.get('SESSION_COOKIE_SECURE'):
        app.logger.warning("SESSION_COOKIE_SECURE should be True in production!")
    
def register_extensions(app):
    """Register Flask extensions."""
    from .extensions import db, migrate, mail, login_manager, cache
    
    db.init_app(app)
    migrate.init_app(app, db)
    mail.init_app(app)
    login_manager.init_app(app)
    cache.init_app(app)
    
    return None

def register_blueprints(app):
    """Register Flask blueprints."""
    from .blueprints.main import main
    from .blueprints.auth import auth
    from .blueprints.admin import admin
    
    app.register_blueprint(main)
    app.register_blueprint(auth, url_prefix='/auth')
    app.register_blueprint(admin, url_prefix='/admin')
    
    return None

def register_errorhandlers(app):
    """Register error handlers."""
    # ... error handler registration ...
    return None

def register_shellcontext(app):
    """Register shell context objects."""
    # ... shell context registration ...
    return None

def register_commands(app):
    """Register Click commands."""
    # ... command registration ...
    return None

This comprehensive example shows how to:

Practical Activity: Implementing Configuration Management

Let's apply what we've learned by implementing a configuration system for a Flask application:

  1. Create a configuration hierarchy for development, testing, and production
  2. Implement environment variable loading with python-dotenv
  3. Set up instance configuration for local overrides
  4. Add configuration validation
  5. Update the application factory to use the configuration

Step 1: Create the Configuration Module

# config.py
import os
from datetime import timedelta

class Config:
    """Base configuration."""
    # Flask
    SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-key')
    
    # Database
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    
    # Application settings
    ITEMS_PER_PAGE = 10
    
    @staticmethod
    def init_app(app):
        """Initialize application with this configuration."""
        pass

class DevelopmentConfig(Config):
    """Development configuration."""
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URI',
                                           'sqlite:///dev.db')

class TestingConfig(Config):
    """Testing configuration."""
    TESTING = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URI',
                                           'sqlite:///:memory:')
    WTF_CSRF_ENABLED = False

class ProductionConfig(Config):
    """Production configuration."""
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI')
    
    # Security settings
    SESSION_COOKIE_SECURE = True
    REMEMBER_COOKIE_SECURE = True

# Dictionary with configuration objects
config = {
    'development': DevelopmentConfig,
    'testing': TestingConfig,
    'production': ProductionConfig,
    'default': DevelopmentConfig
}

Step 2: Create a .env File Template

# .env.example
# Flask
FLASK_ENV=development
SECRET_KEY=your-secret-key

# Database
DEV_DATABASE_URI=sqlite:///dev.db
TEST_DATABASE_URI=sqlite:///:memory:
DATABASE_URI=postgresql://user:password@localhost/app

# Mail
MAIL_SERVER=smtp.example.com
MAIL_PORT=587
MAIL_USERNAME=your-email@example.com
MAIL_PASSWORD=your-email-password

Step 3: Update the Application Factory

# app/__init__.py
import os
from flask import Flask
from config import config
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv()

def create_app(config_name=None):
    """Create and configure the Flask application."""
    app = Flask(__name__, instance_relative_config=True)
    
    # Determine configuration to use
    config_name = config_name or os.environ.get('FLASK_ENV', 'default')
    
    # Load configuration from config object
    app.config.from_object(config[config_name])
    
    # Load instance configuration, if it exists
    app.config.from_pyfile('config.py', silent=True)
    
    # Call init_app method to perform additional setup
    config[config_name].init_app(app)
    
    # Validate configuration
    validate_configuration(app)
    
    # Register extensions
    from app.extensions import db, migrate
    db.init_app(app)
    migrate.init_app(app, db)
    
    # Register blueprints
    from app.blueprints.main import main
    app.register_blueprint(main)
    
    return app

def validate_configuration(app):
    """Validate critical configuration values."""
    # Check for required configuration
    required_config = ['SECRET_KEY', 'SQLALCHEMY_DATABASE_URI']
    for key in required_config:
        if not app.config.get(key):
            raise ValueError(f"Missing required configuration: {key}")
    
    # Warn about insecure settings in production
    if not app.debug and not app.testing:
        if app.config['SECRET_KEY'] == 'dev-key':
            app.logger.warning("Using default SECRET_KEY in production is not secure!")
        
        if not app.config.get('SESSION_COOKIE_SECURE'):
            app.logger.warning("SESSION_COOKIE_SECURE should be True in production!")

Step 4: Add an Instance Configuration Example

# instance/config.py.example
"""
Instance-specific configuration.
Copy this file to config.py and edit as needed.
"""

# Override configuration values here
# These values take precedence over those in the config module
MAIL_SERVER = 'custom-smtp.example.com'
ITEMS_PER_PAGE = 25

Step 5: Update .gitignore

# .gitignore
# Environment variables
.env
.flaskenv

# Instance folder
instance/

# Exclude example files from gitignore
!instance/config.py.example
!.env.example

This implementation provides a solid foundation for configuration management in a Flask application, with support for different environments, local overrides, and configuration validation.

Key Takeaways

Further Learning Resources