Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / flask

Web-Apps on Flask: How to Deal With Cyclic Imports

5.00/5 (1 vote)
22 Apr 2020GPL33 min read 15.9K  
A bit about Python & Flask
Flask is one of the most popular Python frameworks, but some mistakes that occur when using it may lead to certain difficulties. In this article, we will present the topic on how to prevent cyclic imports in a project.

Flask and Cyclic Imports

Developers often face the problem of dependencies between modules while using Flask. To create view and models, a developer uses global objects created and initialized in the main module (in "front controller"). At the same time, there is a risk that cyclic imports will occur and it will be difficult to maintain a project.

Flask documentation and basic tutorials suggest to write a project initialization code in __init__.py to solve the problem. This code creates Flask instance of a class and configures an app. It allows to get access to all global objects from a visibility area of a package.
When using this approach, the structure looks like:

Python
.
├── app
│   ├── __init__.py
│   ├── forms.py
│   ├── models.py
│   ├── views.py
│   └── templates
├── config.py
└── migrations

app/__init__.py

Python
import flask
from flask_mail import Mail
# other extensions

app = Flask(__name__)
mail = Mail(app)
# configure flask app

from app import views, models

app/views.py

Python
from app import app

@app.route('/view_name/'):
def view_name():
     pass

Obviously, this architecture is not so good since all the components are strongly connected. Subsequently, it will be difficult to elaborate such project as changing the code in one place will lead to changes in a dozen of other places.

As a rule, we solve the problem as follows:

  • We avoid standard routing.
  • We prefer original versions of libraries, without "wrapping".
  • Using dependency injection.

Let's focus on this in more depth.

Working With Сlassy

Instead of using the standard routing method described in the documentation, you can use the classy approach. With this approach, you don't have to manually write routing for view: it will be configured automatically based on names of your classes and methods. This approach allows to improve the structure of a code, as well as to create view without app object. As a result, the problem with cyclic import is solved.

The example of the project structure while working with the flask-classful library:

Python
.
├── app
│   ├── static
│   ├── templates
│   ├── forms.py
│   ├── routes.py
│   ├── views.py
│   └── tasks.py
├── models
├── app.py
├── config.py
└── handlers.py

app.py

Python
import flask
from flask_mail import Mail
# other extensions

from app import routes as app_route

app = Flask(__name__)
mail = Mail(app)
# configure flask app

app.register_blueprint(app_route.app_blueprint)

app/routes.py

Python
from flask import Blueprint
from app import views

app_blueprint = Blueprint(...)
views.AccountView.register(app_blueprint)
# register other views

app/views.py

Python
from flask_classy import FlaskView, route
from flask_login import login_required

class AccountView(FlaskView):

     def login(self):
          pass

     # other views

     @login_required
     def logout(self):
          pass

When examining the code, you should pay attention to the fact that initialization now takes place in app.py, which is located at the root. An app is divided into sub projects that are configured by blueprint and then are registered in app object by only one line of code.

Original Libraries are More Preferable

The code presented above shows how flask-classful helps to cope with cyclic imports. This problem in classic Flask projects occurs due to both view declaration and some extensions. One of the best examples is flask-sqlalchemy.
The flask-sqlalchemy extension is designed to improve the integration between sqlalchemy and flask, but in practice it often brings more problems than benefits:

  • The extension promotes the use of a global object for working with the database, including models’ creation, which again leads to the problem of cyclic imports.
  • There is a need to describe models using your own classes, which lead to a tight binding of models to the Flask project. As a result, these models cannot be used in subprojects or in a supporting script.

We try not to use flask-sqlalchemy for these reasons.

Using the Dependency Injection Pattern

The implementation of the classy approach and the rejection of flask-sqlalchemy are just first steps to solve the problem of cyclic import. Next, you need to realise the logic to get an access to global objects in the application. For this purpose, it is good to use the dependency injection pattern implemented in the dependency-injector library.

The example of using a pattern in the code with the dependency-injector library:

app.py

Python
import dependency_injector.containers as di_cnt
import dependency_injector.providers as di_prv
from flask import Flask
from flask_mail import Mail

from app import views as app_views
from app import routes as app_routes

app = Flask(__name__)
mail = Mail(app)

# blueprints registration
app.register_blueprint(app_routes.app_blueprint)

# providers creation
class DIServices(di_cnt.DeclarativeContainer):
    mail = di_prv.Object(mail)

# injection
app_views.DIServices.override(DIServices)

app/routes.py

Python
from os.path import join

from flask import Blueprint

import config
from app import views

conf = config.get_config()

app_blueprint = Blueprint(
    'app', __name__, template_folder=join(conf.BASE_DIR, 'app/templates'),
    static_url_path='/static/app', static_folder='static'
)

views.AccountView.register(app_blueprint, route_base='/')

app/views.py

Python
import dependency_injector.containers as di_cnt
import dependency_injector.providers as di_prv
from flask_classy import FlaskView
from flask_login import login_required

class DIServices(di_cnt.DeclarativeContainer):
    mail = di_prv.Provider()

class AccountView(FlaskView):

    def registration(self):
        # registration implementation
        msg = 'text'
        DIServices.mail().send(msg)

    def login(self):
        pass

    @login_required
    def logout(self):
        pass

The measures mentioned in this article allow to get rid of cyclic imports, as well as to improve the quality of a code. We suggest to look at Flask project using approaches described above, through the example of the "Bulls and cows" game, designed in the form of the web app.

Conclusion

We've considered tips to overcome a common architectural problem of Flask apps related to cyclic imports. Using them, you can simplify maintenance and refactoring of applications.

Thank you for your attention! We hope that this article was useful for you.

History

  • 23rd April, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)