tracy.dev blog

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.


Tracy Chou

Written by Tracy Chou, who spends a lot of time on Twitter.