/ python

Flask is a cancer

Flask is a cancer

Over the course my professional career I have seen many production implementations of Flask and all of them had suffered from the same problems. Today I want to point out these problems and convince you to not choose Flask as the framework for your next application.

The diagnosis

Flask is the 2nd most popular Python framework just after Django. It's popularity comes from excellent documentation and ease of use.

A simple working application can be written in just about 5 lines of code:

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'
    
if __name__ == '__main__':
    app.run()

Which is, of course, a great thing. Unfortunately this seeming easiness is the only thing that Flask has to offer. When your app starts facing real business requirements, the Flask has only simple (and primitive) ways of solving them.
The consequence is that you have to act as if you had chosen badly designed low-level framework for experienced developers.

This is the reason Flask is a trap for developers - they choose it because it promises swift and easy development but after the app gets bigger, they end up with framework architecture that is not suitable for large apps.

Susceptibility

So Flask is a trap, but who is the most likely to get caught in it?
It could be anyone, of course, but I think there are 2 types of people that are particularly vulnerable:

Junior developers - because they think it is easy - just a couple lines of code and it works - "oh, just one file and in the view I can put my code". It also stands to reason that due to them being on the very beginning of their journey with programming they did not yet have time to familiarize themselves with excellent Django documentation.

Senior nonPython developers - because they think they understand the code - "oh, so the app is just an object that you run, no magic framework thing that runs your code" plus they are looking for "micro" framework because Django is big and big is bad.

So they choose Flask over other frameworks like Django or Pyramid.

Design flaws

Now let me enumerate the biggest flaws in Flask architecture.

Global context

Oh yeah, this is the worst thing. In fact, it is so terrible that it could be listed as the sole reason to avoid Flask.It starts quite harmlessly, you have g object so that you can attach any global objects to it and it is there to use in any parts of your application. This concept is OK when you write a toy app but it becomes the source of all evil for an app that is maintained by a team of developers.

The nightmare begins when you start realizing that your code is invoked not only by incoming requests but also:

  • by unit tests
    These split into ones that execute given class or function and the ones that test the whole app by calling given urls.
  • by asynchronous tasks (for example - Celery)
  • by arbitrary scripts (like calculate reports or create some initial db objects)

For each of these, you need to prepare a separate code that bootstraps your global variables.

For unit tests, if you won't unify them, then you could end up with dozen initialization techniques introduced by you and your fellow developers like described in Flask docs (if you need user object, just insert it).

After all, what you really need is to unify ALL entry points to single initialization function, which means you need to fake web request for celery tasks and scripts and pretend that you are always in web request context.

Using SQLAlchemy as ORM

Back in 2012, the SQLAlchemy was superior to Django ORM. But that’s in the past now. From my experience, SQLAlchemy is complex, takes considerable time to master it and even then it can still bite you. Just go to Session Basics section in the SQLAlchemy documentation and see how detailed it is. Then take a peek at State Management and Managing Transactions.
In my opinion, SQLAlchemy can be a good alternative to Django ORM but only in some very, very specific use cases but thus far I was not able to find those in my projects.

No bootstrapping process

What does it mean? You have to bootstrap the app yourself.
The biggest problem is that the Flask mixes imperative and declarative way of configuration and encourages you to do so.

To glue all parts together you end up with ugly Python module that is responsible for bootstrapping the app. Mysterious import order (just try to change it!), ifs everywhere, decorators, ugh.

Example (real project, 5 non-junior developers):

import flask

from myapp.ui import utils
from myapp.ui import admin
from myapp.ui import auth
from myapp.ui import comments
from myapp.ui import infra

app = flask.Flask(__name__)
app.config.update(**config.cfg['flask']['config'])
app.jinja_env.globals['csrf_token'] = csrf.gen_token
app.jinja_env.globals['whois_format'] = config.whois_format
app.json_encoder = config.JSONEncoder
app.session_interface = session.RedisSessionInterface()

# terrible 
if Sentry and 'sentry' in config.cfg:
    sentry_client = Sentry(app, dsn=config.cfg['sentry'])
else:
    sentry_client = None

@app.after_request
def after_request(response):
    # Add custom HTTP response headers
    for (k, v) in config.cfg['flask'].get('headers', {}).items():
        response.headers[k] = v
    return response

@app.teardown_request
def teardown_request(exception=None):
    # Print debug info for longer-lasting requests
    diff = time.time() - flask.g.start_time
    if diff > 3:
        logger.debug('Request "{0}" took {1} second', flask.request.path, diff)

@app.before_request
def before_request():
    flask.g.db = config.Mongo().connect()

    # imperative way of defining things in g, terrible
    if 'user' in flask.session:
        flask.g.username = flask.session['user']['username']
        flask.g.user = auth.User(flask.g.username)
        flask.g.subscription = glask.g.user.get_sub()
        flask.g.profile = flask.g.user.get_profile()
    else:
        flask.g.profile = None
        flask.g.username = None
        flask.g.user = None     
        flask.g.subscription = None
    
    # a lot of code here with many ifs and many many injections to flask.g
    # you already don't really know what is in g

# oh, yeah we need to import these here
import myapp.index
import myapp.amiup
import myapp.svc
import myapp.vanity
import myapp.data.historical
import myapp.admin.account
import myapp.admin.user
import myapp.admin.campaign
import myapp.admin.offer
import myapp.admin.notes
import myapp.admin.servicing
import myapp.admin.operations
import myapp.admin.metro2
import myapp.intern.admin.search.user
import myapp.admin.search.campaign
import myapp.admin.search.offer
import myapp.admin.search.tag
import myapp.admin.search.loan


if app.config.get('debug', False):
    @app.route('/402')
    @app.route('/api/402')
    def test_402():
        flask.abort(402)


    @app.route('/404')
    @app.route('/api/404')
    def test_404():
        flask.abort(404)


    @app.route('/500')
    @app.route('/api/500')
    def test_500():
        flask.abort(500)

How can this be done better?

In Django, the framework bootstraps the app for you: it reads settings file(s) and starts importing your application's modules automatically gluing them together.

Pyramid on the other hand, use dot-notation for specification where the things are, this makes the configuration code much cleaner. Plus it allows you to decorate views like Flask but without using the app object (thanks to venusian library).

Let me give you an example:

from pyramid.config import Configurator
from pyramid.response import Response
from pyramid.view import view_config


# declarative way without need of app object
@view_config(route_name='hello')
def hello(request):
    return Response('Hello')


def hello2(request):
    return Response('Hello2')


def get_app():
    with Configurator() as config:
        config.add_route('hello', '/hello/1')
        config.add_route('hello2', '/hello/2')
        config.add_route('hello3', '/hello/3')

        # dot notation
        config.add_view('.**hello2**', route_name='hello2')
        # dot notation, no need to import module
        config.add_view('myapp.views.hello3', route_name='hello3')

        # really nice way for loading extensions:
        config.include('pyramid_jinja2')
        config.include('pyramid_tm')
        config.include('pyramid_retry')

        # our extensions:
        config.include('.models')
        config.include('.routes')

        # scan step, it adds hello view
        config.scan()
        app = config.make_wsgi_app()
    return app

No middlewares

A middleware is a great concept - easy to understand and very powerful. This is the reason most of the frameworks implement them.

Example in Django:

class SimpleMiddleware(object):
    def __init__(self, get_response):
        # One-time configuration and initialization.
        self.get_response = get_response

    def __call__(self, request):
        # Code to be executed for each request before
        # the view (and later middleware) are called.

        response = self.get_response(request)

        # Code to be executed for each request/response after
        # the view is called.

        return response

What we have in Flask? @before_request and @after_request decorators.
Not only they encourage you to write one big ugly function that does everything before and after the request as opposed to feature-specific middlewares but also:

  • it is hard to discover the execution order
  • it is hard to run logic that spans both before and after the request

No permissions

I have seen a dozen custom implementations of permissions logic in Flask. This proves my point that Flask is chosen as a framework for beginners but then it forces the developer to write quite difficult code around it. As a sidenote permissions have solid implementation both in Django and Pyramid out-of-the-box.

Hard to write proper unit tests

This is caused by Flask using Global Context. If you have request context, script context and, celery context then you have to think in which context to run given test.
If you don't prepare your testing framework at the very beginning of app develpment then it is really hard to do so later.

3 false reasons

There are 3 reasons you can think that Flask is the right framework for you, let me pick them apart one by one and present alternatives.

  1. choosing Flask because you are a beginner
    This is a simple case, just ignore Flask, use Django. Django is an excellent, mature and easy framework.
  2. choosing Flask because you are not able to use SQL
    Ok, fair enough. Triple check that you really can't use SQL database, then choose Pyramid.
  3. choosing Flask because you need something small and fast
    If the performance is really an issue then choosing any other Python framework would probably not help.
    If the problem you are trying to solve is CPU bound then you should probably use low-level language like C/C++/Rust and create python bindings to it.
    IO bound problems should be solved using asyncio + uvloop or even Go or Javascript language.
    If you need micro framework just for sake of library being small, then it is not a valid requirement - just use Django.

Wrapping Things Up

I was not able to find a single reason why to use Flask, if you have one, please let me know in the comments down below. I’d be happy to prove you wrong :)

Flask is a cancer
Share this