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.
Directory structure
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 app.py
.
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
Files
Drumroll please… the app.py
file.
# 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()
The 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 from
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
That’s all
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.