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.
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...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.