Flask Application Factory: Isolated Instances for Testing and Deployment
Use Flask's application factory to create isolated app instances for testing and deployment, with a corrected config-loading example and verification steps.
23 Oct 2025, 12:50 UTC

The problem: one app object, shared state
Many Flask tutorials start with app = Flask(__name__) at module level. That single object is created when the module is imported, so database setup, CLI commands, and extension initialization run once for the whole process. If you need a test database, a development database, and a production database, you end up manipulating a shared global or writing fragile setup and teardown code. Tests can leak configuration into each other, and running multiple app instances in one process becomes impractical.
Thesis: an application factory isolates each instance
Moving app creation into a function—commonly create_app()—lets you build a fresh Flask instance for every environment or test. You pass configuration into the function, initialize extensions with their init_app() method, register blueprints, and return the app. This pattern is well established in Flask and works with the framework's CLI and test client.
How the factory works (Flask 2.3 and later)
The examples below assume Flask 2.3 or later. In these releases, FLASK_ENV is no longer used; use flask run --debug or FLASK_DEBUG=1 instead. Flask calls create_app without arguments when you set FLASK_APP to it, so the function must work with no arguments or provide defaults for every parameter.
- Define the factory. Create a
Flaskobject inside the function, load configuration, initialize extensions, register blueprints, and return the app. - Use
from_mapping()for dictionaries.app.config.from_object()reads attributes from a class or module; it does not interpret a Python dict as key-value settings. Pass dicts tofrom_mapping()instead. - Initialize extensions with
init_app(). This avoids binding an extension to an app at import time and prevents circular imports. - Keep app-dependent code inside the factory. CLI commands, error handlers, and before-request functions that need the app should be defined or registered after the app object exists.
Worked example
All commands assume you are in the project root and have activated a virtual environment with Flask and Flask-SQLAlchemy installed.
# myproject/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
def create_app(config_object=None):
"""Application factory."""
app = Flask(__name__)
app.config.from_mapping(
SECRET_KEY='dev',
SQLALCHEMY_DATABASE_URI='sqlite:///app.db',
SQLALCHEMY_TRACK_MODIFICATIONS=False,
)
if config_object is not None:
if isinstance(config_object, dict):
app.config.from_mapping(config_object)
else:
app.config.from_object(config_object)
db.init_app(app)
from . import routes
app.register_blueprint(routes.bp)
@app.cli.command('init-db')
def init_db():
"""Create database tables."""
with app.app_context():
db.create_all()
print('Initialized the database.')
return app
# myproject/routes.py
from flask import Blueprint
bp = Blueprint('main', __name__)
@bp.route('/')
def index():
return 'Hello, factory.'
The route returns a plain string so the example does not depend on a template file you would need to create separately.
Run the development server
- Export the factory to Flask's CLI:
export FLASK_APP=myproject:create_app
flask run --debug
Required permissions: read access to the project files and execute permission on your virtual environment's flask script. No special system privileges are needed.
Expected check: the server starts and listens on http://127.0.0.1:5000. Run curl -I http://127.0.0.1:5000/ and look for HTTP/1.0 200 OK.
Risk: if create_app requires a mandatory argument, flask run will fail because Flask invokes the factory with no arguments. Provide defaults or use a small wrapper function that calls create_app with your chosen settings.
Test configuration isolation with pytest
# tests/conftest.py
import pytest
from myproject import create_app
@pytest.fixture
def app():
return create_app({
'TESTING': True,
'SQLALCHEMY_DATABASE_URI': 'sqlite:///:memory:',
})
@pytest.fixture
def client(app):
return app.test_client()
# tests/test_index.py
def test_index(client):
rv = client.get('/')
assert rv.status_code == 200
assert rv.data == b'Hello, factory.'
Run pytest from the project root. Each test receives a fresh app instance, so configuration changes in one test do not carry over to another.
Verify that instances do not share database state
You can confirm that two factory calls produce independent SQLAlchemy engines by comparing their URLs:
from myproject import create_app
app1 = create_app({'SQLALCHEMY_DATABASE_URI': 'sqlite:///db1.sqlite'})
app2 = create_app({'SQLALCHEMY_DATABASE_URI': 'sqlite:///db2.sqlite'})
print(app1.extensions['sqlalchemy'].engine.url)
print(app2.extensions['sqlalchemy'].engine.url)
The two URLs should differ. Accessing .engine may initialize the engine, so run this check in a throwaway script rather than inside a test suite unless you intend to create those engines.
Trade-offs and limitations
- The factory adds indirection. Newcomers may find it harder to locate where a CLI command or extension is initialized, especially if the factory is split across modules.
flask runcalls the factory with no arguments. If your factory requires arguments, you must supply defaults or register a custom CLI entry point.- Extensions initialized with
init_app()are designed to be reusable, but some extensions store app-specific state. Confirm each extension's documentation if you create many instances in one process.
Actionable closing
If you are starting a new Flask project, begin with create_app() rather than a module-level app. Move extension initialization to init_app() calls, load dictionaries with from_mapping(), and point FLASK_APP at the factory. Verify by running the server, executing pytest, and comparing engine URLs for two instances. The pattern costs a small amount of structure up front and pays off in cleaner tests and simpler deployment configuration.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.