How to mitigate `import` hell in Flask
March 02, 2019
I’m sincerely baffled as to why every Flask tutorial recommends an app initialization pattern essentially guaranteed to cause circular import issues. After experimenting with a few different approaches, I’ve finally found a pattern that seems to work, which I’m documenting here for future-me. Future-me, you’re welcome.
By way of background
Here is an example of the wretched app init pattern that has never worked for me (this one is adapted from the Flask-SQLAlchemy 2.3 quickstart guide, 2019 March 2):
# File: app.py from flask import Flask from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db' db = SQLAlchemy(app)
The recommendation is then to access the database instance like so:
# File: user_model.py from app import db class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False) def __repr__(self): return '<User %r>' % self.username
The issue is that the base
app.py file needs to register routing, handlers,
and everything else to make the app run, ultimately something in there needs to
access the database connection or models and tries to do
from app import db,
as in the above code snippet, and voila, circular import errors!
A better way
After much gnashing of the teeth and pulling of the hair, and many desperate,
usually futile attempts to move
import calls inside function definitions, I
have arrived upon the following pattern for my Flask apps.
I still have an
app.py file in charge of initializing the Flask app, but it
imports all the extensions it needs from a separate
extensions.py file. As for
route handlers in different files, they are saved on blueprints that are also
then explicitly registered by
This sample directory structure is for an app that implements both web views as well as API endpoints.
project/ app.py config.py extensions.py api.py webapp.py models/ user_model.py
Drumroll please… the
# File: app.py from flask import Flask from config import config def create_app(): app = Flask(__name___) app.config.from_object(config) register_extensions(app) register_blueprints(app) return app def register_extensions(app): from extensions import bootstrap from extensions import db from extensions import jwt_manager from extensions import login_manager from extensions import migrate bootstrap.init_app(app) db.init_app(app) migrate.init_app(app, db) jwt_manager.init_app(app) login_manager.init_app(app) login_manager.login_view = 'webapp.login' def register_blueprints(app): from api import api_bp from webapp import webapp_bp app.register_blueprint(api_bp) app.register_blueprint(webapp_bp) app = create_app()
extensions.py file takes care of instantiating all the extensions to be
added to the Flask app, including the database. This means that, for example, if
any other callers need to access the database connection, they can call
extensions import db instead of having to do
from app import db, which was
such a source of heartache in the prior setup.
# File: extensions.py from flask_bootstrap import Bootstrap from flask_jwt_extended import JWTManager from flask_login import LoginManager from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy bootstrap = Bootstrap() jwt_manager = JWTManager() login_manager = LoginManager() migrate = Migrate() db = SQLAlchemy()
The files that register the API and webapp routes look something like this:
# File: api.py from flask import Blueprint from flask_jwt_extended import jwt_required from graphql_api import graphql_schema api_bp = Blueprint('api', __name__) @api_bp.route('/api/graphql/', methods=['POST']) @jwt_required def graphql(): view = GraphQLView.as_view( 'graphql', schema=graphql_schema.schema, graphiql=True) return view()
# File: webapp.py from flask import Blueprint from flask import redirect from flask import render_template from flask_login import current_user from flask_login import login_user from webapp_forms import LoginForm # Some custom form. webapp_bp = Blueprint('webapp', __name__) @webapp_bp.route('/') def home(): if current_user.is_authenticated: return render_template('home.html') else: return render_template('unauth_home.html') @webapp_bp.route('/login/', methods=['GET', 'POST']) def login(): from models.user_model import User if current_user.is_authenticated: return redirect(url_for('webapp.home')) form = LoginForm() if form.validate_on_submit(): user = User.query.filter_by(email=form.email.data).first() if user is None or not user.check_password(form.password.data): error = 'Invalid credentials. Please try again.' return render_template('login.html', form=form, error=error) login_user(user, remember=form.remember_me.data) return redirect(url_for('webapp.home')) return render_template('login.html', form=form)
For completeness, here is a sample config file:
# File: config.py class Config: JWT_SECRET_KEY = os.environ['JWT_SECRET_KEY'] SECRET_KEY = os.environ['SECRET_KEY'] SQLALCHEMY_DATABASE_URI = os.environ['DATABASE_URL'] SQLALCHEMY_TRACK_MODIFICATIONS = False config = Config
Some day I’ll properly understand how Python imports work, but this is good enough for now.
P.S. I thought about titling this post “How to avoid `import` hell in Flask”, but I realized I could not promise to avoid it, only to try to mitigate it, hence my mitigated language. Hah.
Written by Tracy Chou, who spends a lot of time on Twitter.