Configuration Management

Difficulty: Intermediate

Proper configuration management is crucial for Flask applications that need to work across development, testing, and production environments. Flask provides several ways to load configuration, and following best practices ensures your app is secure and deployable.

Flask's app.config is a subclass of Python's dict with additional methods for loading configuration from various sources. You can set values directly with app.config['KEY'] = value, load from a Python object with app.config.from_object(), load from a file with app.config.from_pyfile(), or load from environment variables.

The most common pattern is config classes. You define a base Config class with shared settings, then subclass it for each environment. DevelopmentConfig enables debug mode, TestingConfig uses an in-memory database, and ProductionConfig loads secrets from environment variables. The create_app factory selects the appropriate config.

Environment variables are the recommended way to handle secrets in production. Database URLs, API keys, secret keys, and other sensitive values should never be in source code. Use os.environ.get() to read them, and python-dotenv to load them from a .env file during development. The .env file must be in .gitignore.

Flask's from_envvar() method loads an entire config file whose path is stored in an environment variable. This is useful when your deployment system provides a config file. The from_mapping() method loads config from a dictionary, which is convenient for testing.

The instance folder (instance/) is a special directory that Flask treats differently. Files in this folder are deployment-specific and should not be committed to version control. Place your production config file, database, or uploaded files here. Access it with app.instance_path and load config from it with app.config.from_pyfile('config.py', instance_relative=True).

Common Flask configuration values include: SECRET_KEY (session signing), SQLALCHEMY_DATABASE_URI (database connection), DEBUG (debug mode), TESTING (test mode), MAX_CONTENT_LENGTH (upload size limit), JSON_SORT_KEYS (JSON key ordering), and SERVER_NAME (for URL generation outside requests).

Code examples

Configuration Classes

# config.py
import os

class Config:
    """Base configuration."""
    SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-fallback')
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    MAX_CONTENT_LENGTH = 16 * 1024 * 1024  # 16 MB

class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///dev.db'

class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
    WTF_CSRF_ENABLED = False

class ProductionConfig(Config):
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
    SESSION_COOKIE_SECURE = True
    SESSION_COOKIE_HTTPONLY = True

config = {
    'development': DevelopmentConfig,
    'testing': TestingConfig,
    'production': ProductionConfig,
    'default': DevelopmentConfig,
}

Config classes use inheritance. The base class defines shared settings. Each subclass overrides environment-specific values. A config dict maps names to classes for easy selection in the factory.

Loading Configuration in the Factory

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

def create_app(config_name=None):
    app = Flask(__name__, instance_relative_config=True)

    # Determine config
    if config_name is None:
        config_name = os.environ.get('FLASK_CONFIG', 'development')

    # Load from config class
    from config import config
    app.config.from_object(config[config_name])

    # Override with instance config (if exists)
    app.config.from_pyfile('config.py', silent=True)

    # Override with environment variables
    if os.environ.get('SECRET_KEY'):
        app.config['SECRET_KEY'] = os.environ['SECRET_KEY']

    # Ensure instance folder exists
    os.makedirs(app.instance_path, exist_ok=True)

    return app

The factory loads config in layers: base config class, then instance config (deployment-specific overrides), then environment variables. This layered approach provides flexibility for different deployment scenarios.

Using python-dotenv

# .env (not committed to git)
FLASK_APP=app
FLASK_DEBUG=1
SECRET_KEY=super-secret-production-key
DATABASE_URL=postgresql://user:pass@localhost/mydb
REDIS_URL=redis://localhost:6379/0
MAIL_SERVER=smtp.gmail.com
MAIL_USERNAME=myapp@gmail.com
MAIL_PASSWORD=app-specific-password

# .gitignore
.env
instance/
venv/

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

load_dotenv()  # Load .env file

def create_app():
    app = Flask(__name__)
    app.config['SECRET_KEY'] = os.environ['SECRET_KEY']
    app.config['SQLALCHEMY_DATABASE_URI'] = os.environ['DATABASE_URL']
    return app

python-dotenv loads variables from .env into os.environ. This keeps secrets out of code. The .env file is gitignored and each developer has their own. In production, set environment variables directly.

Validating Configuration

from flask import Flask
import os

def create_app():
    app = Flask(__name__)

    # Load config
    app.config.from_object('config.ProductionConfig')

    # Validate required config values
    required_keys = ['SECRET_KEY', 'SQLALCHEMY_DATABASE_URI']
    missing = [key for key in required_keys
               if not app.config.get(key)]

    if missing:
        raise RuntimeError(
            f'Missing required config keys: {", ".join(missing)}. '
            f'Set them as environment variables or in the config file.'
        )

    # Warn about insecure defaults
    if app.config['SECRET_KEY'] == 'dev-fallback':
        import warnings
        warnings.warn('Using default SECRET_KEY. Set a proper key in production!')

    return app

Validating configuration at startup catches misconfigurations early. Check for required keys and warn about insecure defaults. This prevents cryptic runtime errors when a missing database URL causes a crash later.

Key points

Concepts covered

Config Object, from_object, Environment Variables, from_envvar, Instance Folder