Flask Application Factory — Core Concepts
The problem the factory solves
In a basic Flask tutorial, you write app = Flask(__name__) at module level. Every other file imports app to register routes, access config, or use extensions. This creates a web of circular imports: the app module needs blueprints, but blueprints need the app.
Worse, that module-level app is a singleton. You can’t easily create a second instance with different config. Tests contaminate each other because they share state. Running a Celery worker with different settings means hacking around the existing app object.
The application factory moves app creation into a function, breaking every one of these problems.
The basic pattern
def create_app(config_name='default'):
app = Flask(__name__)
app.config.from_object(configs[config_name])
# Initialize extensions
db.init_app(app)
migrate.init_app(app, db)
# Register blueprints
from .auth import auth_bp
from .main import main_bp
app.register_blueprint(auth_bp)
app.register_blueprint(main_bp)
return app
Notice that blueprint imports happen inside the function. This is deliberate. At import time, the app doesn’t exist yet, so blueprints can’t depend on a global app object. They depend on db, migrate, and other extensions that are created without an app and initialized later via init_app().
How extensions work with factories
Flask extensions follow a two-phase initialization pattern:
- Create the extension without an app:
db = SQLAlchemy() - Initialize it inside the factory:
db.init_app(app)
Between creation and initialization, the extension exists but isn’t bound to any app. This lets multiple files import db without needing the app, breaking circular dependencies.
The extension stores per-app state using Flask’s app.extensions dictionary, so one extension instance can serve multiple app instances simultaneously — important for testing.
Configuration management
The factory accepts a configuration parameter, enabling environment-specific setup:
configs = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
'default': DevelopmentConfig
}
Each config class sets different values: TestingConfig uses an in-memory SQLite database, ProductionConfig reads the database URL from environment variables. The factory doesn’t care which one it gets — it applies whatever it’s given.
How Flask discovers the factory
When you set FLASK_APP=myapp, Flask looks for:
- An
appobject inmyapp - A
create_app()function inmyapp - An
apporcreate_app()inmyapp/__init__.py
Flask calls create_app() automatically during flask run. You can pass arguments using FLASK_APP="myapp:create_app('testing')".
Testing with factories
Factories make testing straightforward:
@pytest.fixture
def app():
app = create_app('testing')
with app.app_context():
db.create_all()
yield app
db.drop_all()
Each test gets a completely fresh app. No shared state, no contamination between tests. You can even run tests in parallel because each creates its own isolated app instance.
Common misconception
“The factory pattern is only for big projects.” Even a small Flask app benefits from a factory. The moment you write your first test, you need a second app instance. The moment you add a CLI command, you need the app created in a predictable way. Starting with a factory costs nothing and saves refactoring later.
What the factory doesn’t solve
A factory creates configured apps, but it doesn’t manage the lifecycle. You still need to handle:
- Worker processes — Gunicorn calls the factory per worker
- Database connections — Connection pools are per-app, created during
init_app - Background tasks — Celery needs its own
create_appcall to get an app context
The factory is the foundation, not the entire architecture. Combine it with blueprints for routing, service layers for logic, and proper deployment tooling for production.
One thing to remember: The factory breaks the circular import problem by delaying app creation. Extensions exist without an app, blueprints are imported inside the factory, and each call produces a fresh, independently configured instance.
See Also
- Python Django Admin Get an intuitive feel for Django Admin so Python behavior stops feeling unpredictable.
- Python Django Basics Get an intuitive feel for Django Basics so Python behavior stops feeling unpredictable.
- Python Django Celery Integration Why your Django app needs a helper to handle slow jobs in the background.
- Python Django Channels Websockets How Django can send real-time updates to your browser without you refreshing the page.
- Python Django Custom Management Commands How to teach Django new tricks by creating your own command-line shortcuts.