DEV Community

loading...
Cover image for Structure Flask project with convention over configuration

Structure Flask project with convention over configuration

Roman Imankulov
Originally published at roman.pt ・3 min read

When people ask me what a should beginner Python developer choose, Flask or Django, to start their new web project, all other things being equal, I always recommend Django.

Unlike Flask, Django is opinionated. In other words, every time you need to make a choice in Flask, Django has the answer for you. Among others, Django outlines the practical project structure out of the box.

With Django, you split your project into applications. The applications themselves have a well-defined structure: views.py, models.py, all those things. What maintains and enforces the design is the convention over configuration approach: you put your code to specific well-known locations, and the framework discovers them.

Flask doesn’t impose anything like that. You can start with a single file. When the application grows, it’s up to you to provide the application structure. As a result, the app can quickly turn into an unmaintainable mess.

Official Flask documentation, following the non-opinionated principle, leaves it for developers to decide. Chapters Larger Applications Modular Applications with Blueprints don’t cover the topic entirely. To close the gap, people created their own guidelines. Google “flask project structure” to find a plethora of variants and suggestions. For example, there is a chapter a better application structure in the monumental Flask Mega-tutorial.

What bugs me about any Flask solution is the lack of support for conventions. If you’re like me, you have a function app() that manually imports and configures all the things from all the packages and blueprints. Looks familiar? This is dirty.

# file: myproject/app.py

def app():
    from myproject.users.controller import blueprint as users_blueprint
    from myproject.projects.controller import blueprint as projects_blueprint
    ...
    # Initialize extensions
    db.init_app(flask_app)

    ...
    # Register blueprints
    flask_app.register_blueprint(users_blueprint)
    flask_app.register_blueprint(projects_blueprint)
    ...
Enter fullscreen mode Exit fullscreen mode

For every extension, you call init_app. For every application with a well-defined structure, you make a dance, adding a couple of lines with imports and registrations.

To somehow address the issue, I created a Python package roman-discovery. The package lets you declaratively define the conventions of your application and run a discover() function to apply their rules. It's not specific to Flask, but I created it primarily with Flask in mind.

For example, assuming that you store all the blueprints in the myproject/<app>/controllers.py, that’s how you can automatically register all of them.

from flask import Blueprint
from roman_discovery import ObjectRule, discover
from roman_discovery.matchers import MatchByPattern, MatchByType

blueprint_loader = ObjectRule(
    name="Flask blueprints loader",
    module_matches=MatchByPattern(["myproject.*.controllers"]),
    object_matches=MatchByType(Blueprint),
    object_action=flask_app.register_blueprint,
)

discover(import_path="myproject", rules=[blueprint_loader])
Enter fullscreen mode Exit fullscreen mode

There is a roman_discovery.flask module you can use as a source of inspiration, or, if you don’t mind applying my conventions, use it as is.

from roman_disovery.flask import discover_flask

app = Flask(__name__)
app.config.from_object("myproject.config")
discover_flask("myproject", app)
Enter fullscreen mode Exit fullscreen mode

The latest line will do the following.

  • Scan myproject/*/controllers.py and myproject/*/controllers/*.py to find blueprints and attach them to the Flask application.
  • Import all files in myproject/*/models.py and myproject/*/models/*.py to help flask-migrate find all the SQLAlchemy models to create migrations.
  • Scan all files in myproject/*/cli.py and myproject/*/cli/*.py to find flask.cli.AppGroup instances and attach them to Flask’s CLI.
  • Scan top-level myproject/services.py, find all the instances that have init_app() methods, and call obj.init_app(app=app) for each of them.

It’s still new, lacks documentation, examples, and tests, but I hope it can already become helpful for you.

Follow roman-discovery on GitHub, also to know why the package has an uncommon “roman-” prefix.

GitHub logo imankulov / roman-discovery

Discover packages and classes in a python project

Roman Discovery

The package scans the project to execute some actions with found modules and objects. It's specifically helpful for frameworks that define resources on the fly with decorators and expect you to import all necessary modules.

For example, it can be helpful for Flask to load all your blueprints, initialize extensions, and import SQLAlchemy models.

Install

pip install roman-discovery
Enter fullscreen mode Exit fullscreen mode

Usage with Flask

Using within the opinionated Flask structure was the initial purpose of the package. Use the roman_discovery.flask.discover_flask() function.

The function expects the following project structure.

myproject
  app.py
  config.py
  services.py
  foo/
    controllers.py
    models.py
    cli.py
  bar/
    controllers/
      api.py
      admin.py
    models/
      users.py
      projects.py
    cli/
      user_commands.py
      project_commands.py

With this structure, it will do the following.

  • Scan controllers.py and controllers/ to find blueprints and attach the blueprints to the flask application.
  • Import all files in models.py and models/ to help flask-migrate find all the SQLAlchemy models to create migrations.
  • Scan cli.py and cli/…

Discussion (2)

Collapse
codingsafari profile image
Nico Braun • Edited

I found the application factory approach less practical when running the app in a container. There I found it most effective to initialize the app straight up in the main file based on environment variables. So that each container can easiliy be configured with orchestration tools.

What are your thoughts about configuration in container? Did you find any good approach? Or would you maybe even add different config files via volume and use the factory pattern?

Collapse
imankulov profile image
Roman Imankulov Author

Environment variables are the way to go 🚀!

Having a config.py doesn't mean that I don't use environment variables or initialize configuration from secrets. Instead, config provides an abstraction layer that hides the configuration source.

My current preferred setup consists of three files at the top of the project.

  • config.py: acts mainly as a proxy to environment variables.
  • services.py: a service registry that other parts of the code use heavily.
  • app.py: the module with a function initializing the project.

In the app.py, I wrap initialization with a function to avoid accidental initialization on import.

Here's my boilerplate.

GitHub logo imankulov / flask-boilerplate

Flask project boilerplate