Why an Application Factory?
An application factory is a function (commonly named create_app) that builds and returns a Flask application instance. Instead of creating a global app = Flask(__name__) at import time, you create the app when you need it. This enables:
- Multiple environments: create apps with different configs (development, testing, production) without changing code.
- Testability: tests can create isolated app instances with in-memory or temporary resources.
- Safer imports: modules can be imported without side effects (like connecting to a database on import).
- Better composition: extensions, blueprints, and feature flags can be wired in a predictable order.
Step-by-step: Build create_app
1) A minimal factory
The factory should accept a config selector and optionally a dict of overrides for tests.
# app/__init__.py
from flask import Flask
def create_app(config_name: str | None = None, overrides: dict | None = None) -> Flask:
app = Flask(__name__, instance_relative_config=True)
# Load default config (base)
app.config.from_object("app.config.BaseConfig")
# Load environment-specific config
if config_name:
app.config.from_object(f"app.config.{config_name}")
# Load instance config file if present (optional)
# e.g., instance/config.py
app.config.from_pyfile("config.py", silent=True)
# Apply explicit overrides last (useful for tests)
if overrides:
app.config.update(overrides)
# Ensure instance folder exists
try:
app.instance_path # triggers resolution
except Exception:
pass
# Register components
register_extensions(app)
register_blueprints(app)
configure_logging(app)
validate_config(app)
return app
def register_extensions(app: Flask) -> None:
# Example placeholder: init db, cache, etc.
# db.init_app(app)
pass
def register_blueprints(app: Flask) -> None:
# Example placeholder
# from .routes import bp
# app.register_blueprint(bp)
pass
def configure_logging(app: Flask) -> None:
pass
def validate_config(app: Flask) -> None:
passinstance_relative_config=True tells Flask that instance-specific files live under the instance/ folder (outside the package), which is ideal for per-machine secrets and environment overrides.
2) Selecting the environment
In production you typically select the config via an environment variable. A common pattern is:
# wsgi.py (or similar entrypoint)
import os
from app import create_app
config_name = os.getenv("FLASK_CONFIG", "DevelopmentConfig")
app = create_app(config_name)This keeps environment selection out of your package code and makes it easy to run multiple instances with different settings.
- Listen to the audio with the screen off.
- Earn a certificate upon completion.
- Over 5000 courses for you to explore!
Download the app
Configuration Objects: Base + Development/Testing/Production
Define configuration as Python classes. Use a BaseConfig for shared defaults and specialized subclasses for environment-specific behavior.
# app/config.py
import os
def env_bool(name: str, default: bool = False) -> bool:
raw = os.getenv(name)
if raw is None:
return default
return raw.strip().lower() in {"1", "true", "yes", "on"}
def env_int(name: str, default: int) -> int:
raw = os.getenv(name)
if raw is None:
return default
return int(raw)
class BaseConfig:
# Security
SECRET_KEY = os.getenv("SECRET_KEY", "dev-only-insecure")
# Feature toggles
DEBUG = env_bool("DEBUG", False)
TESTING = False
# Logging
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
# Database
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///app.db")
# Example of a required external service URL (optional by default)
EXTERNAL_API_URL = os.getenv("EXTERNAL_API_URL", "")
class DevelopmentConfig(BaseConfig):
DEBUG = True
LOG_LEVEL = os.getenv("LOG_LEVEL", "DEBUG")
class TestingConfig(BaseConfig):
TESTING = True
DEBUG = False
# Use an isolated DB for tests by default
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///:memory:")
# Make secrets deterministic for tests
SECRET_KEY = os.getenv("SECRET_KEY", "test-secret")
class ProductionConfig(BaseConfig):
DEBUG = False
# In production, you typically want INFO or WARNING
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")How defaults should behave
Defaults should be sensible for local development and tests, but you should avoid silently using insecure defaults in production. A practical approach is:
- Allow a weak default
SECRET_KEYonly in development/testing. - In production, fail fast if critical variables are missing.
We’ll implement that fail-fast behavior in validate_config later.
Loading Configuration from Environment Variables (Safely)
Environment variables are the standard way to inject settings into a running service (containers, systemd, CI). Key practices:
- Parse types: booleans and integers should be parsed, not treated as strings.
- Prefer explicit names: e.g.,
DATABASE_URL,LOG_LEVEL,SECRET_KEY. - Don’t log secrets: never print full connection strings if they include passwords.
Example: toggling features
With the config above, you can toggle behavior without code changes:
| Setting | Env var | Example value | Effect |
|---|---|---|---|
| Debug mode | DEBUG | true | Enables debug features (only for dev) |
| Logging level | LOG_LEVEL | WARNING | Controls verbosity |
| Database URL | DATABASE_URL | postgresql+psycopg://... | Switches DB backend/host |
| Config selection | FLASK_CONFIG | ProductionConfig | Selects config class |
Instance Folder: Per-environment Settings Without Committing Secrets
The instance/ folder is designed for deployment-specific files that should not live in version control. Common uses:
- Local development overrides (
instance/config.py) - SQLite database files (
instance/app.sqlite) - Certificates or other machine-local artifacts
Because the factory uses instance_relative_config=True and calls from_pyfile("config.py", silent=True), you can create an instance/config.py on a specific machine to override settings.
Example: instance/config.py
# instance/config.py
# This file should not be committed.
SECRET_KEY = "a-long-random-secret"
DATABASE_URL = "sqlite:///instance/app.sqlite"
LOG_LEVEL = "DEBUG"Precedence matters. With the factory shown earlier, the effective order is:
- Base config class
- Selected environment config class
instance/config.py(if present)- Explicit overrides passed to
create_app(tests)
Handling Secrets Safely
Secrets include SECRET_KEY, API tokens, and database passwords. Practical rules:
- Never hardcode secrets in repository code.
- Prefer environment variables for secrets in production.
- Use instance config for local secrets if you don’t want to export env vars.
- Fail fast in production if a secret is missing or insecure.
If your database URL includes credentials, treat it as sensitive. When logging config, redact values.
# app/security.py
from urllib.parse import urlsplit, urlunsplit
def redact_url(url: str) -> str:
if not url:
return url
parts = urlsplit(url)
if parts.username or parts.password:
netloc = parts.hostname or ""
if parts.port:
netloc += f":{parts.port}"
return urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment))
return urlConfig-driven Logging Setup
Use LOG_LEVEL to control verbosity. Keep logging configuration inside the factory so each app instance configures itself correctly.
# app/__init__.py (logging part)
import logging
def configure_logging(app):
level_name = app.config.get("LOG_LEVEL", "INFO")
level = getattr(logging, level_name.upper(), logging.INFO)
# Basic configuration; in real services you may add JSON formatters/handlers
logging.basicConfig(level=level)
# Reduce noisy loggers if needed
logging.getLogger("werkzeug").setLevel(level)Improving Testability with Factory Overrides
Tests can create an app with TestingConfig and override specific settings (like using a temporary directory).
# tests/conftest.py
import tempfile
import pytest
from app import create_app
@pytest.fixture
def app():
with tempfile.TemporaryDirectory() as tmp:
app = create_app(
"TestingConfig",
overrides={
"DATABASE_URL": "sqlite:///:memory:",
"SECRET_KEY": "test-secret",
},
)
yield app
@pytest.fixture
def client(app):
return app.test_client()This pattern avoids global state and makes it easy to run tests in parallel.
Fail-fast Validation: Verify Config Correctness at Startup
A small validation function can prevent misconfigured deployments from starting. The goal is to detect missing required variables early and clearly.
Implement validate_config
# app/__init__.py (validation part)
def validate_config(app):
errors = []
# Example: SECRET_KEY must be set securely in production
if app.config.get("ENV") == "production" or app.config.get("FLASK_ENV") == "production":
# Note: Flask 2.3+ deprecates FLASK_ENV usage; keep checks explicit to your deployment.
pass
# Better: infer production by selected config class name
config_obj = app.config.get("CONFIG_NAME", "")
# If you set CONFIG_NAME yourself, you can use it here.
# Alternatively, pass a boolean like IS_PRODUCTION in ProductionConfig.
is_production = app.config.get("IS_PRODUCTION", False)
if is_production:
if not app.config.get("SECRET_KEY") or app.config["SECRET_KEY"] in {"dev-only-insecure", "test-secret"}:
errors.append("SECRET_KEY is missing or insecure for production")
db_url = app.config.get("DATABASE_URL", "")
if not db_url:
errors.append("DATABASE_URL is required in production")
# Example: validate LOG_LEVEL
valid_levels = {"CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"}
if str(app.config.get("LOG_LEVEL", "INFO")).upper() not in valid_levels:
errors.append("LOG_LEVEL must be one of CRITICAL, ERROR, WARNING, INFO, DEBUG")
if errors:
# Raising stops startup immediately (fail-fast)
raise RuntimeError("Config validation failed: " + "; ".join(errors))Make production detection explicit
Rather than guessing production from Flask internals, set a flag in your config classes:
# app/config.py
class ProductionConfig(BaseConfig):
IS_PRODUCTION = True
DEBUG = False
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
class DevelopmentConfig(BaseConfig):
IS_PRODUCTION = False
DEBUG = True
class TestingConfig(BaseConfig):
IS_PRODUCTION = False
TESTING = TrueThen validation becomes reliable and easy to reason about.
Startup Checklist (Config Correctness)
- Config selection: confirm
FLASK_CONFIG(or your equivalent) points to the intended config class. - Required variables present: in production, ensure
SECRET_KEYandDATABASE_URLare set and non-empty. - No insecure defaults in production: reject placeholder secrets like
dev-only-insecure. - Type correctness: booleans/ints parsed correctly (e.g.,
DEBUGis boolean, not"false"). - Logging level valid:
LOG_LEVELis one of the allowed values. - Secrets not logged: redact sensitive URLs/tokens if you print diagnostics.
- Fail-fast behavior: validation raises an error before serving requests when config is invalid.