Free Ebook cover Flask Essentials: Practical Backend Patterns for Small Services

Flask Essentials: Practical Backend Patterns for Small Services

New course

14 pages

Flask Essentials: Application Factory Pattern and Configuration Management

Capítulo 2

Estimated reading time: 8 minutes

+ Exercise

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:
    pass

instance_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 App

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_KEY only 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:

SettingEnv varExample valueEffect
Debug modeDEBUGtrueEnables debug features (only for dev)
Logging levelLOG_LEVELWARNINGControls verbosity
Database URLDATABASE_URLpostgresql+psycopg://...Switches DB backend/host
Config selectionFLASK_CONFIGProductionConfigSelects 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:

  1. Base config class
  2. Selected environment config class
  3. instance/config.py (if present)
  4. 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 url

Config-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 = True

Then 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_KEY and DATABASE_URL are set and non-empty.
  • No insecure defaults in production: reject placeholder secrets like dev-only-insecure.
  • Type correctness: booleans/ints parsed correctly (e.g., DEBUG is boolean, not "false").
  • Logging level valid: LOG_LEVEL is 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.

Now answer the exercise about the content:

In a Flask application factory setup, which configuration source should have the highest precedence so tests can reliably override settings like DATABASE_URL and SECRET_KEY?

You are right! Congratulations, now go to the next page

You missed! Try again.

The factory loads config in layers. To make tests deterministic, explicit overrides should be applied last, after base, environment-specific, and instance config, so they take highest precedence.

Next chapter

Flask Essentials: Blueprints for Modular Routing and API Organization

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.