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:
- Separate code from configuration, making your application more maintainable
- Keep sensitive information secure and out of version control
- Easily adapt your application to different environments
- Simplify deployment and scaling
- Support different settings for testing
Think of configuration as the control panel for your application - it lets you adjust how your application behaves without changing the code itself.
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.
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
- Never Store Secrets in Version Control: Even if the repository is private, it's not secure for sensitive data.
- Use Environment Variables: Store secrets as environment variables, not in configuration files.
- Provide Default Values for Development: Use non-sensitive defaults for development, but don't rely on them for production.
- Rotate Secrets Regularly: Change sensitive keys and passwords periodically, especially after team member changes.
- 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:
- AWS Secrets Manager
- Google Cloud Secret Manager
- Azure Key Vault
- HashiCorp Vault
- Docker secrets (for containerized applications)
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:
- Type validation for configuration values
- Default values and required fields
- Custom validation logic for specific settings
- Automatic loading from environment variables or .env files
- IDE auto-completion and type hints
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:
- Clear separation of configuration for different environments
- Inheritance to avoid duplication
- Environment-specific initialization logic
- Configuration in code, which can be versioned and reviewed
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:
- Use an in-memory SQLite database
- Disable CSRF protection for easier form testing
- Set TESTING = True to enable testing features
- Disable email sending
- Use mocked services instead of real external APIs
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:
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:
- Organize configuration for different environments
- Load configuration from different sources
- Validate configuration values
- Initialize applications with environment-specific settings
- Properly handle sensitive information
Practical Activity: Implementing Configuration Management
Let's apply what we've learned by implementing a configuration system for a Flask application:
- Create a configuration hierarchy for development, testing, and production
- Implement environment variable loading with python-dotenv
- Set up instance configuration for local overrides
- Add configuration validation
- 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
- Proper configuration management separates code from configuration, making applications more maintainable and secure.
- Flask provides multiple methods for loading configuration, including from objects, files, and environment variables.
- Using the application factory pattern allows for flexible configuration across different environments.
- Environment variables are a secure way to manage sensitive configuration, especially in production.
- Configuration classes with inheritance provide a clean way to organize configuration for different environments.
- Validating configuration at startup helps prevent runtime errors and security issues.
- Type-safe configuration using libraries like Pydantic adds robustness to configuration management.
- Instance folders provide a way to override configuration locally without affecting version control.
- Testing often requires specific configuration, which can be managed through fixtures or test-specific configuration classes.