Configuration and project structure
Organise config as classes, read secrets from the environment, keep machine-specific files in the instance folder, and make development and production differ only where they must.
Config objects
# app/config.py
import os
from datetime import timedelta
class Config:
SECRET_KEY = os.environ.get("SECRET_KEY") # None -> sessions break loudly
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL")
SQLALCHEMY_TRACK_MODIFICATIONS = False
MAX_CONTENT_LENGTH = 5 * 1024 * 1024
PERMANENT_SESSION_LIFETIME = timedelta(hours=8)
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = "sqlite:///" + os.path.join(os.getcwd(), "dev.db")
class TestingConfig(Config):
TESTING = True
DEBUG = False
SECRET_KEY = "test-only-not-a-secret"
SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:" # or a dedicated test database
WTF_CSRF_ENABLED = False
class ProductionConfig(Config):
DEBUG = False
SESSION_COOKIE_SECURE = True
PREFERRED_URL_SCHEME = "https"
CONFIGS = {
"development": DevelopmentConfig,
"testing": TestingConfig,
"production": ProductionConfig,
}# app/__init__.py
import os
from .config import CONFIGS
def create_app(config_object=None):
app = Flask(__name__, instance_relative_config=True)
name = config_object or os.environ.get("FLASK_CONFIG", "development")
app.config.from_object(CONFIGS[name] if isinstance(name, str) else name)
app.config.from_pyfile("config.py", silent=True) # instance/config.py wins
return app
# tests pass a class directly
app = create_app("testing")| Key | Used for | Typical value |
|---|---|---|
SECRET_KEY | Signing sessions and flash messages | Long random bytes from the environment |
DEBUG | Reloader and interactive debugger | True in development only |
TESTING | Propagates exceptions instead of returning 500 | True in the test config |
SQLALCHEMY_DATABASE_URI | Database connection | SQLite locally, PostgreSQL in production |
MAX_CONTENT_LENGTH | Largest accepted request body | A few MB, matching the proxy limit |
SESSION_COOKIE_SECURE | Send session cookie over HTTPS only | True in production |
PREFERRED_URL_SCHEME | Scheme used by url_for(_external=True) | https in production |
Environment variables and the instance folder
# app/config.py — fail fast, in one place
import os
def required(name):
try:
return os.environ[name]
except KeyError:
raise RuntimeError(f"Missing required environment variable: {name}")
class ProductionConfig(Config):
DEBUG = False
SECRET_KEY = required("SECRET_KEY")
SQLALCHEMY_DATABASE_URI = required("DATABASE_URL")
# local development: load a git-ignored .env before the app reads config
from dotenv import load_dotenv
load_dotenv() # in run.py or wsgi.py, never in production images
# typed values: strings from the environment are always strings
DEBUG = os.environ.get("DEBUG", "").lower() in {"1", "true", "yes"}
MAX_CONTENT_LENGTH = int(os.environ.get("MAX_UPLOAD_MB", "5")) * 1024 * 1024app.instance_pathpoints at a directory outside the package where local, machine-specific files live:config.pywith local overrides, an upload folder, a SQLite file. It is not copied into the installed package and not committed.- Later sources win:
from_objectthenfrom_pyfilemeans the instance file overrides the class. Use that ordering deliberately so a developer can point at a local database without editing tracked code. - Give a secret no default in production. A missing
SECRET_KEYthat silently falls back to a string in the source means every deployment shares signing keys, and an attacker who reads the repository can forge session cookies. - Never commit a
.envfile. Provide a.env.examplewith placeholder values so a new checkout knows what to set.
Development, test and production
# tests/conftest.py
import pytest
from app import create_app
from app.extensions import db
@pytest.fixture
def app():
application = create_app("testing")
with application.app_context():
db.create_all()
yield application
db.session.remove()
db.drop_all()
@pytest.fixture
def client(app):
return app.test_client()export FLASK_CONFIG=production
export SECRET_KEY="$(python -c 'import secrets; print(secrets.token_hex(32))')"
export DATABASE_URL="postgresql+psycopg://app:pw@db:5432/app"
flask --app app:create_app run⚠️
A configuration object with
DEBUG = True written into tracked source is a production incident waiting for a copy-paste. The Werkzeug debugger executes arbitrary Python from a browser form, and its traceback pages expose source files, configuration values and environment variables. Keep DEBUG off in every class that is not DevelopmentConfig, drive the choice from the FLASK_CONFIG environment variable, and check the running value rather than the file you think was deployed.FAQ
Where should the database URL come from?
The environment in production, a local default in development. Never hard-code credentials in a config class that is committed — the value ends up in the repository history, in image layers and in any error report that prints the config.
Why use config classes instead of app.config[...] assignments?
Classes give you a named, importable set of values, an obvious place to see the differences between environments, and something a test can pass into
create_app(). Scattered assignments hide the defaults and make it impossible to build a second app with different settings.Related
Blueprints and the application factory Deployment: gunicorn, nginx and Docker
Last refreshed 2026-09-18.