Hacker News new | past | comments | ask | show | jobs | submit login

Flask's route decorator gives a nice syntax, but it goes against some ideal best practices:

* Imports shouldn't have side-effects (like registering functions with flask).

* You shouldn't use globals (like the flask app).

* Objects (such as the flask app) should be immutable whenever possible.

None of these are hard-and-fast rules, and Python code has a tendency to give up purity in favor of syntax, so it's certainly justified for Flask to be designed this way, but it's still a bit unsettling, and can lead to bugs, especially in larger cases when your handlers are split up across many files. Some examples:

* You need to make sure that you import every file with a request handler, and those imports often end up unused (only imported for their side-effects), which confuses linters and other static analysis tools.

* It's also easy to accidentally import a new file through some other import chain, so someone rearranging imports later might accidentally disable part of your app by never importing it.

* It can break some "advanced" uses of modules/imports, such as the reload function.

* Test code and scripts that want access to your request handlers are forced to build a (partial) Flask app, even if they have no use for one.

At my job, I recently changed our Flask handlers to be registered with a different approach (but the same API) that avoids most of these issues. Rather than setting things up with side-effects, it makes the route details easy to introspect later. Here's what our implementation of @route() looks like now:

  def route(rule, **options):
      def route_decorator(func):
          # Attach the route rule to the request handler.
          func.func_dict.setdefault('_flask_routes', []).append((rule, options))
  
          # Add the request handler to this module's list of handlers.
          module = sys.modules[func.__module__]
          if not hasattr(module, '_FLASK_HANDLERS'):
              module. _FLASK_HANDLERS = {}
          module._FLASK_HANDLERS[func.__name__] = func
          return func
  
      return route_decorator
So if you have a module called user_routes.py, with 3 Flask request handlers, then user_routes._FLASK_HANDLERS is a list containing those three functions. If one of those handlers is user_routes.create_user, then you can access user_routes.create_user._flask_routes in order to see the names of all of the route strings (usually just one) registered for that request handler.

Then, in separate code, there's a list of all modules with request handlers, and we import and introspect all of them as part of a function that sets up and returns the Flask app. So outside code never has any way of accessing a partially-registered Flask app, imports of request handler modules are "pure", and request handlers can often be defined without depending on Flask at all.




>Imports shouldn't have side-effects (like registering functions with flask).

Imports don't have side-effects. Using Flask's route decorators has.

>You shouldn't use globals (like the flask app).

The Flask app is just as much a global as any other class instance in any OOP language. Whether you make it a module-level object or not is your choice.

>Objects (such as the flask app) should be immutable whenever possible.

They hardly are. This is a good rule which nobody follows, and I don't think you'd gain enough advantages through this.

>You need to make sure that you import every file with a request handler, and those imports often end up unused (only imported for their side-effects), which confuses linters and other static analysis tools.

The fact that your app has import side-effects is your fault, this pattern is not at all encouraged by Flask. You probably want to use blueprints.


You're right, I hadn't seen blueprints, and they do seem to address my concerns pretty nicely.

All of the examples that I've seen (including the Flask quickstart guide, the linked post, and everything I could find on http://flask.pocoo.org/community/poweredby/ ) work by assigning the Flask app to a module-level variable (i.e. a global), then using that global at import time for all @app.route usages, so my assumption has been that that's the encouraged style. It at least seems to be pretty common. But I guess none of those examples are very big (only a few split the request handlers across multiple files), so they didn't get to a point where blueprints would be especially useful.

(Also, to be clear, when I say "imports shouldn't have side-effects", what I mean is that top-level code (which runs at import time) should ideally have no side effects.)


Check out blueprints[1], they pretty much do what you just talked about.

[1]: http://flask.pocoo.org/docs/0.10/blueprints/




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: