Restructuring Flask Apps with the Application Factory Pattern and Blueprints
Module-level Flask objects lock configuration at import time and make testing brittle. A create_app factory plus blueprints fixes isolation and circular imports.
12 Oct 2026, 04:22 UTC

When a Flask project starts small, the default pattern is seductive: drop app = Flask(__name__) at the top of a module, decorate a few views, and run it. That approach works until you need a second, differently configured instance—say, one for tests with a temporary database, or one for a CLI command that should not boot the web server. At that point, importing the module to reach the app object triggers route registration and extension setup before you have chosen any configuration. You are stuck with whatever the module-level code decided.
The fix is to stop creating the app at import time. Define a create_app(config=None) factory that instantiates Flask, applies configuration through from_object or from_mapping, calls init_app on each extension, registers blueprints, and returns the app. Entry points and tests call it explicitly.
Why module-level app objects hurt
Decorators such as @app.route execute at import time. That means the moment any module imports your app object, routes are registered and extensions are initialized with whatever configuration the module happened to set. Tests cannot create an isolated instance with a temporary database or a different secret key. CLI commands and background workers that import the app to reuse its configuration inadvertently trigger the full web startup path. The import itself becomes a side effect, and the application’s shape is fixed before any caller can intervene.
The factory and blueprint pattern
Move app construction into create_app. Extension instances like db = SQLAlchemy() or cache = Cache() remain module-level, but without an app. The factory binds them with init_app, so the same object can serve the web process, CLI commands, and background workers.
Route modules should define a Blueprint(...) and never import the app object. Because the blueprint is just a collection of deferred operations, the factory can safely import it: from myapp.api import bp inside create_app does not trigger circular imports. URL prefixes are wired in one place with app.register_blueprint(bp, url_prefix=...).
A worked refactor
Assume a package myapp with an auth blueprint. The factory lives in myapp/__init__.py:
# myapp/__init__.py
from flask import Flask
from .config import Config
from .extensions import db, cache
def create_app(config=None):
app = Flask(__name__)
if config is None:
app.config.from_object(Config)
else:
app.config.from_mapping(config)
db.init_app(app)
cache.init_app(app)
from .auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix="/auth")
return app
The blueprint module has no reference to the app:
# myapp/auth.py
from flask import Blueprint
bp = Blueprint("auth", __name__)
@bp.route("/login")
def login():
return "Login"
A pytest fixture can now build an isolated instance. Note that app.app_context() covers code that needs an application context outside a request; code relying on request, session, or g still needs test_request_context() or a real request.
# tests/conftest.py
import pytest
from myapp import create_app
@pytest.fixture
def client():
app = create_app({"TESTING": True, "SECRET_KEY": "test-secret"})
with app.test_client() as client:
with app.app_context():
yield client
Trade-offs, verification, and rollback
Factory detection by flask run is a Flask 1.0+ behavior. On older installs, point FLASK_APP at a module that builds the app at import time instead.
- More indirection. Readers must follow the factory to understand startup. The payoff is explicit configuration.
- Missing
init_app. Forgetting to bind an extension surfaces as a runtime error, often phrased as the application not being registered with the extension, rather than at import time. - Endpoint names change. Blueprint routes are namespaced. A view formerly reachable as
url_for('login')becomesurl_for('auth.login'). Templates and tests referencing bare names will raiseBuildErrorafter the refactor. - Global app imports. Any module doing
from myapp import appmust be refactored to useflask.current_app. - Config precedence.
from_mappingoverridesfrom_object. Load secrets from the environment last and never commit them to factory defaults.
Run these checks from your project root, where myapp is importable:
List routes. Run
flask --app myapp:create_app routes. Confirm blueprint-qualified endpoints such asauth.loginand the expected URL prefixes appear.Prove test isolation. Add a route that returns a config flag, for example a
/healthzendpoint returningapp.config["TESTING"]. Exercise it through the pytest fixture to confirm the test instance uses the injected mapping, not the development config.Inspect the URL map. In a Python shell, run
from myapp import create_app; app = create_app(); print(app.url_map)to inspect the final rule set without booting a server.Smoke test the CLI. Start the app with
python -m flask --app myapp:create_app runand request one blueprint route plus a nonexistent path to confirm error handling still resolves.
If a check fails, the most common causes are a blueprint imported before init_app has run, a stale url_for call using the old endpoint name, or a config key expected by an extension that the factory did not set. Because the factory is additive, you can migrate one blueprint at a time. To roll back, restore the previous module-level app object and its decorators for the affected module.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.