13

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?

Remco Haszing
  • 1,399
  • 2
  • 11
  • 18

1 Answers1

12

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.

  • 2
    Here's what I think Armin means by "badly supported": In Python, reloading a module doesn't reload the modules that that module imports, nor does it reassociate names in other modules from pointing to old objects to new ones from the new module - so hot swapping a new module into the same process is problematic. You're far better off starting a new process when you want to make a code change. – Aaron Hall Aug 03 '16 at 21:04
  • Now that you mention it, I do recall the behaviour you described, thank you for the clarification! I'll edit the answer accordingly. – Martin Jungblut Schreiner Aug 04 '16 at 00:06
  • ok, plus 1 for citing me. :) – Aaron Hall Aug 04 '16 at 01:18
  • The addition from Aaron Hall clarified it for me. Thanks. :) – Remco Haszing Aug 11 '16 at 09:18
  • @RemcoHaszing I'm not sure why that whole middle section is there. The Werkzeug reloader does not use Python's `reload` mechanism. It reloads by running a separate process, no matter if it's started from `app.run` or `flask run`. There's no hot swapping going on, Aaron is wrong. – davidism Aug 21 '16 at 14:25
  • @davidism but has it always been like this? And if that's the case, why has the Flask documentation been updated, including this very reason for people to use Flask's CLI, rather than .run()? Not doubting you, just trying to understand. – Martin Jungblut Schreiner Aug 22 '16 at 18:45
  • It has always been like that. The Werkzeug reloader has never used Python's `reload` mechanism. Not sure what you mean by your second question, it's answered in the first quote in your answer. None of those reasons are related to Python's `reload` (they couldn't be, it's not used). – davidism Aug 22 '16 at 18:46
  • Ok, wait, now I got actually confused. I see now how `reload` has nothing to do with this, since it was never used, but what is the actual reason for Flask to recommend CLI now, rather than .run()? I want to edit the answer accordingly not to leave it displaying incorrect information to other users. – Martin Jungblut Schreiner Aug 22 '16 at 19:08
  • 7
    Why do we have to use the environment variable `FLASK_APP`? Is that intrinsic to how this works? I'm curious why `flask run` doesn't accept the same as an argument, which would make onboarding newcomers easier. Thank you. – John Wheeler Sep 06 '16 at 01:47
  • Do you feel it more like an advice ? Or are they trying to force the use of CLI. Many apps might want to use flask for exposing some of they features via REST. I guess from within python they would be tempted to call .run instead of calling a shell script that will comeback again to the python in flask. – yucer Apr 24 '19 at 13:57
  • I also see that there is no intention to support the new FLASK_APP parameter from the run method (https://github.com/pallets/flask/issues/3095) – yucer Apr 24 '19 at 13:58