Why Flask CLI Is Recommended Over Flask.run

flaskpython

In Flask 0.11 a flask CLI was introduced. Both the docs and the changelog state this is recommended.

Development Server docs:

Starting with Flask 0.11 there are multiple built-in ways to run a development server. The best one is the flask command line utility but you can also continue using the Flask.run() method.

Command Line

The flask command line script (Command Line Interface) is strongly recommended for development because it provides a superior reload experience due to how it loads the application. The basic usage is like this:

$ export FLASK_APP=my_application
$ export FLASK_DEBUG=1
$ flask run

Changelog:

  • Added flask and the flask.cli module to start the local
    debug server through the click CLI system. This is recommended over the old flask.run() method as it works faster and more reliable due to a
    different design and also replaces Flask-Script.

So far I didn't notice this "superior reload experience". I fail to see the point of using the CLI over a custom script.

If using Flask.run, I would simply write a python file:

#!/usr/bin/env python3
from my_app import app


if __name__ == '__main__':
    app.run(debug=True)

If using the CLI, one would have to specify environment variables. In the CLI docs is stated that this can be integrated in the activate script of virtualenvwrapper. Personally I consider this to be part of the application and think it should be under version control. Alas, a shell script is needed:

#!/usr/bin/env bash
export FLASK_APP=my_app:app
export FLASK_DEBUG=1

flask run

Of course this will be accompanied by an additional bat script as soon as any Windows users start to collaborate.

Also the first option allows setup written in Python before starting the actual app.

This allows for example

  • to parse command line arguments in Python
  • to setup logging before running the app

They seem to promote that it's possible to add custom commands. I fail to see why this is better than writing simple Python scripts, optionally exposed through entry points.

Example logging output when using a configured logger using the Python run script:

$ ./run.py 
   DEBUG 21:51:22 main.py:95) Configured logging
    INFO 21:51:22 _internal.py:87)  * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
    INFO 21:51:22 _internal.py:87)  * Restarting with inotify reloader
   DEBUG 21:51:22 main.py:95) Configured logging
 WARNING 21:51:22 _internal.py:87)  * Debugger is active!
    INFO 21:51:22 _internal.py:87)  * Debugger pin code: 263-225-431
   DEBUG 21:51:25 inotify_buffer.py:61) in-event <InotifyEvent: src_path=b'my_app/main.py', wd=272, mask=IN_MODIFY, cookie=0, name=b'main.py'>
   DEBUG 21:51:25 inotify_buffer.py:61) in-event <InotifyEvent: src_path=b'my_app/main.py', wd=272, mask=IN_MODIFY, cookie=0, name=b'main.py'>
    INFO 21:51:25 _internal.py:87)  * Detected change in 'my_app/main.py', reloading
    INFO 21:51:26 _internal.py:87)  * Restarting with inotify reloader
   DEBUG 21:51:26 main.py:95) Configured logging
 WARNING 21:51:26 _internal.py:87)  * Debugger is active!
    INFO 21:51:26 _internal.py:87)  * Debugger pin code: 263-225-431

Example logging output when using a configured logger using the CLI:, notice that the root logger couldn't be setup early enough in the process.

$ ./run.sh 
 * Serving Flask app "appsemble.api.main:app"
 * Forcing debug mode on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with inotify reloader
   DEBUG 21:51:33 main.py:95) Configured logging
 * Debugger is active!
 * Debugger pin code: 187-758-498
   DEBUG 21:51:34 main.py:95) Configured logging
   DEBUG 21:51:37 inotify_buffer.py:61) in-event <InotifyEvent: src_path=b'my_app/main.py', wd=272, mask=IN_MODIFY, cookie=0, name=b'main.py'>
   DEBUG 21:51:37 inotify_buffer.py:61) in-event <InotifyEvent: src_path=b'my_app/main.py', wd=272, mask=IN_MODIFY, cookie=0, name=b'main.py'>
 * Detected change in 'my_app/main.py', reloading
    INFO 21:51:37 _internal.py:87)  * Detected change in 'my_app/main.py', reloading
 * Restarting with inotify reloader
    INFO 21:51:38 _internal.py:87)  * Restarting with inotify reloader
 * Debugger is active!
 * Debugger pin code: 187-758-498
   DEBUG 21:51:38 main.py:95) Configured logging

My actual question is simply:

Why is flask CLI recommended over Flask.run?

Best Answer

In the development server docs, they state there are issues with calling run() and automatically reloading code:

This works well for the common case but it does not work well for development which is why from Flask 0.11 onwards the flask method is recommended. The reason for this is that due to how the reload mechanism works there are some bizarre side-effects (like executing certain code twice, sometimes crashing without message or dying when a syntax or import error happens).

They claim the CLI doesn't suffer from this problem.

The first commit that seems to touch on this issue is this: https://github.com/pallets/flask/commit/3bdb90f06b9d3167320180d4a5055dcd949bf72f

And there Armin Ronacher wrote:

It is not recommended to use this function for development with automatic reloading as this is badly supported. Instead you should be using the flask command line script's runserver support.

As mentioned by Aaron Hall, it seems like the use of run() could be problematic due to the fact that all objects which are instances of classes defined in the modules being replaced won't be reinstantiated, and whenever a module is reloaded, the modules it imports aren't reloaded as well.

The details about this may be found for Python 3 at: https://docs.python.org/3/library/importlib.html?highlight=importlib#module-importlib

It states:

As with all other objects in Python the old objects are only reclaimed after their reference counts drop to zero.

Other references to the old objects (such as names external to the module) are not rebound to refer to the new objects and must be updated in each namespace where they occur if that is desired.

When a module is reloaded, its dictionary (containing the module’s global variables) is retained. Redefinitions of names will override the old definitions, so this is generally not a problem. If the new version of a module does not define a name that was defined by the old version, the old definition remains.

So, by creating a new process and killing the old one, you naturally eliminate all obsolete references.

Also, Flask's CLI uses the 'click' module, making it very easy to add custom commands, but most importantly, besides fixing the reloading bug, the CLI offers a standardised way to run applications and add custom commands. This sounds like a very good thing, because it makes familiarity with Flask more transferable between different teams and applications, rather than having multiple ways to do the same thing.

It seems like a genuine way to make Flask more in accordance to the Zen of Python:

There should be one-- and preferably only one --obvious way to do it.

Related Topic