My Experience In Production with: Flask, Bottle, Tornado and Twisted


I’ve been using flask, bottle, tornado in production day to day for years, writing plugins for these and maintaining a variety of new or legacy applications that happened in Big Corp TM.

Here’s my take on the different frameworks, having used them extensively and see many many people use them and ask for advice on what to use.

Before we start, what they all have in common -except twisted- is they’re widely used and well-established web frameworks with extensive documentation.

Flask

Pro:

  • It’s quick to start with.
  • It’s (not) simple. (It’s really not)
  • It’s well documented.

Con:

  • It’s a crazy mess of global variables
  • It’s really inconsistent.
  • Unrelated dependencies.
  • Zero multithread support, compression, async, etc…

Flask example with unit test

from flask import Flask, request, make_response
import json

app = Flask(__name__)


@app.route("/")
def hello():
    return "Hello World!"


@app.route("/api", methods=["POST"])
def api():
    content = {"language": request.headers.environ.get("HTTP_ACCEPT_LANGUAGE", "")}
    response = make_response(content)
    response.headers["Content-Type"] = "application/json"
    return response


def main():
    app.run(port=8888)


if __name__ == "__main__":
    main()

import unittest
import examples.example_flask
import json


class MyTestCase(unittest.TestCase):
    def setUp(self):
        self.app = examples.example_flask.app.test_client()

    def test_hello(self):
        response = self.app.get("/")
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data.decode("utf-8"), "Hello World!")

    def test_api(self):
        response = self.app.post("/api", headers={"Accept-Language": "en"})
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.headers["Content-Type"], "application/json")

        obj = json.loads(response.data)
        self.assertEqual(obj["language"], "en")

    def test_api_bad_method(self):
        response = self.app.get("/api", headers={"Accept-Language": "en"})
        self.assertEqual(response.status_code, 405)

if __name__ == '__main__':
    unittest.main()

My Experience with Flask

Flask is fine for a trivial application with a single python file and a few endpoints. Anything past that and you’re going to feel the pain.

Flask has zero structure. It’s a mess of global variables and path hell.

With a few files, you very quickly loose where are the actual endpoints are? how did any request got there? and why the headers are in request.header when there’s never been a variable request defined in this entire scope? WTF???

Then there’s the problem of the construction of flask itself. Flask is a bunch of API mashed up on top of other libraries doing the work, the biggest dependency being werkzeug. You will learn about that soon enough as you try to make a flask application.

Flask dependencies don’t collaborate together. This will hit you at least once a year when you try to upgrade and things break. Too bad request.headers from flask is a werkzeug.datastructures object and the object has changed!

Last but not least. It doesn’t support features like async, compression, workers, etc… we will get back to this on a dedicated section.

IMO Flask is a perfect example of a quick and dirty framework hacked together, that really comes back to haunt you beyond a toy project. I’ve found repeatedly that whatever flask can do, bottle and tornado can do it better more consistently and easier to maintain.

Bottle

Pro:

  • It’s quick to start with.
  • It’s well documented.
  • Entirely self contained. A single file with no dependency.
  • Extremely consistent.

Con:

  • Zero multithread support, compression, async, etc…
  • There are a few globals like flask but not as bad.

Bottle example with unit test

The example below is demonstrating the organized way. There is an alternative way by abusing global variables, very similar to Flask.

from bottle import Bottle, run
import json
import bottle

class MyApp(bottle.Bottle):
    def __init__(self):
        super(MyApp, self).__init__()
        self.route('/',    method="GET",  callback=self.hello_handler)
        self.route('/api', method="POST", callback=self.api_handler)

    def hello_handler(self):
        return "Hello World!"

    def api_handler(self):
        response = {"language": bottle.request.headers.environ.get("HTTP_ACCEPT_LANGUAGE", "")}
        bottle.response.set_header("Content-Type", "application/json")
        return json.dumps(response)

def main():
    app = MyApp()
    app.run(host="localhost", port=8888)


if __name__ == "__main__":
    main()

import unittest
import examples.example_bottle
import json
import webtest


class MyTestCase(unittest.TestCase):
    def setUp(self):
        self.app = webtest.TestApp(examples.example_bottle.MyApp())

    def test_hello(self):
        response = self.app.get("/")
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.body.decode("utf-8"), "Hello World!")

    def test_api(self):
        response = self.app.post("/api", headers={"Accept-Language": "en"})
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.headers["Content-Type"], "application/json")

        obj = json.loads(response.body)
        self.assertEqual(obj["language"], "en")

    def test_api_bad_method(self):
        response = self.app.get("/api", headers={"Accept-Language": "en"}, status=405)
        self.assertEqual(response.status_code, 405)

if __name__ == '__main__':
    unittest.main()

My Experience with Bottle

Bottle can have more structure compared to flask. It doesn’t rely entirely on global variable (though there are some).

There is a handful of global variables you could abuse similarly to flask but there’s no need to do this, you can declare endpoints sanely and there is a more comprehensible request flow.

Little known but very important, bottle is a single python file bottle.py. If you’re ever stuck in a pseudo-embedded project where you can’t really use dependencies (pip and co), you can drop bottle right in and get a best-of-the-world web framework and IT’S AWESOME. (This came really handy on one or two highly constrained projects I’ve been on).

Bottle is extremely consistent. By design of course and by the will of the authors.

It simply cannot break due to arbitrary dependencies and changes because there is no such thing! It’s the antithesis of Flask in many ways and it’s great.

One issue though. It doesn’t support features like async, compression, workers, etc… we will get back to this on a dedicated section.

I had to (re)write a lot of authentication code in a big corp, occasionally testing on a large old legacy project that’s very used (if it works with that it will be reusable for anything). It went pretty well only because BOTTLE IS ORGANIZED and never broke (that’s not to say the partial rewriting was easy). If you wonder what happens to similar flask projects for comparison, they’re in various states of abandonment because they’re impossible to maintain.

My Longer Take on Bottle vs Flask

Disclaimer: Don’t go thinking that I hate flask and love bottle. They’re just tools to me and if anything my love belongs to tornado and my archenemy is twisted. We will get into them later.

Note that the provided examples don’t go into advanced usage like hook or authentication where things get real messy real quick in Flask.

I’ve seen companies/departments built on a flask and bottle applications growing to 50k+ LOC over the course of 5-10 years. They both have rough edges around performance (python multithreaded/async story *cough cough*) and they both accumulated a ton of legacy/business cruft (real world project with real world users *cough cough*). Both written by decent developers with some tests.

The bottle is likely chugging along fine and development continues. There may be some rough edges and swear words thrown around (ever seen a new graduate NOT shocked on their first enterprise project? 😀) but work goes on relatively well.

The flask is likely to be a disaster in comparison and might be objectively killing the company/project, complexity hell has brought development and maintenance to a crawl. The word of mouth is to go for a rewrite from scratch.

This is real war story. Multiple stories actually, the most notable was at a startup whose main website was a very large flask creation that was ultimately thrown array and rewritten from scratch. It was the right thing to do and it went fine, the company was able to expand afterwards and to go mobile and to print more money.

Let’s not judge too quick, that house of cards might be a twisted project.

Tornado

Pro:

  • It’s quick to start with
  • Properly organized endpoints
  • No third party dependencies, all features are built-in.
  • Compression support.
  • Async and yield support (optional)
  • WebSocket (if you need that)

Con:

  • A tad of boilerplate to write.
  • Async is not trivial, don’t use if you don’t need.

Tornado Example with unit test

Tip: The general practice is to subclass tornado.web.RequestHandler to have some shared code executed before all requests or change how errors are treated.

import tornado.ioloop
import tornado.web
import json


class HelloHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello World!")


class ApiHandler(tornado.web.RequestHandler):
    def post(self):
        response = {"language": self.request.headers.get("Accept-Language", "")}
        self.set_header("Content-Type", "application/json")
        self.write(json.dumps(response))


def make_app():
    return tornado.web.Application([
        (r"/", HelloHandler),
        (r"/api", ApiHandler),
    ])


def main():
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()


if __name__ == "__main__":
    main()

import examples.example_tornado
import tornado.testing
import json

class MyTestCase(tornado.testing.AsyncHTTPTestCase):
    def get_app(self):
        return examples.example_tornado.make_app()

    def test_hello(self):
        response = self.fetch("/")
        self.assertEqual(response.code, 200)
        self.assertEqual(response.body.decode("utf-8"), "Hello World!")

    def test_api(self):
        response = self.fetch("/api", method="POST", body="", headers={"Accept-Language": "en"})
        self.assertEqual(response.code, 200)
        self.assertEqual(response.headers["Content-Type"], "application/json")

        obj = json.loads(response.body)
        self.assertEqual(obj["language"], "en")

    def test_api_bad_method(self):
        response = self.fetch("/api", raise_error=False)
        self.assertEqual(response.code, 405)

My Experience with Tornado

Tornado is most notably the only maintainable framework for anything with more than a few files, because it’s the only one that actually force you to structure the project. (One could say it’s object oriented because you have to write handler class that are given request objects -not global variables-).

Requests are given to a handler, the handler(s) do what they look like they do, the details of the request is available in the object (self.request.headers self.request.path etc). There is no hack with global variables and magic flow (flask could really send a HTTP request to ANY python function anywhere in the repo).

There’s a bit of boilerplate to write (some people speak ill of tornado because of that) but it makes it very easy to grasp when you read it back next month or when you join a new project, which is priceless as a professional developer. Code is read a thousand times more than it is written, readability trumps everything else.

Must-have features from a web framework

More importantly it’s the only full featured framework that cover workers, HTTP compression, async support, websocket, an httpclient, various performance / non-blocking considerations, etc… all critically lacking from the alternatives.

These are important for any real software development that will go past a few users.

  • Are you planning to serve more than 100 users?
    • Python is mono threaded, it has to scale by having multiple processes to handle more than one request at once (workers model). Tornado can do workers out of the box.
  • Are you serving static files or text or json that might be multi megabytes?
    • How about compression? Tornado can compress response out of the box (80%-90% smaller typically). It’s night and day in load time.
  • Does any request depends on calling another service?
    • Tornado can easily make a non blocking call to that one API. It’s nice to not delay all requests for one call.
  • [There’s more advanced features but we will stop there]

All these makes tornado the only reasonable choice for web development. Assuming you’re planning to have more than a toy project (almost always true in an established company with an existing userbase), you will find yourself requiring these really bad really soon.

The strongest advise I could give you is to go with tornado from the start, so you don’t paint yourself in a corner.

I’ve stopped counting how many people went with X and were stuck due to blocking/nonforking/nonperformance, and the most reasonable thing to do was to redo the web service into tornado. [Changing HTTP framework is very realistic for an early stage project with a few endpoints that’s already hitting major issues. The more you wait the harder it gets.]

A successful migration to Tornado: edited: The migration story was too good so will be published as a separate article.

Twisted

Pro:

Con:

(and you thought I had a thing against Flak, I said I don’t, Flask has rough edges but it’s functional for a micro server)

My personal experience with twisted

What is twisted? It’s a bunch of stuff including utilities, frameworks and tools to make more stuff, typically stuff involving custom network protocols in highly specific asynchronous distributing systems.

It’s almost a language inside python, or a factory of factory if you will (java developers will appreciate the comparison). It’s possible to write an API server in the sense that it’s Turing complete not in the sense that it’s a good idea.

One example that I have seen made in twisted for reference (I think so but I could be wrong) is a distributed job scheduler distributing jobs onto a compute grid.

Think. This includes clients to send tasks, servers to execute the tasks, distributed among thousands of nodes, tasks themselves that might be anything, the whole thing is nuts and entirely custom. It’s a job feasible with twisted, 10 years ago.

Use Cases:

  • If you’re looking to do that sort of things. Good luck! (Doubt whether twisted is a good choice for that in 2020).
  • If you’re looking to make a web server. Bottle will serve you well (or flask or anything else).
  • If you’re looking to make a non trivial web server where asynchronicity is required. Tornado will serve you well.
  • If you’re looking for an asynchronous HTTP client. Tornado has one or there’s a number of dedicated libraries (ask google).

A bit of history

twisted was one of the first attempt to get non blocking IO into Python, specifically for use cases involving network IO (distributed systems).

Python has limited support for non-blocking IO, especially before python 3 and the async keyword. Twisted tried to work around that, maybe 10 years earlier.

Twisted is a huge undertaking having to work around the python interpreter and the language to achieve the goal. It’s big, it’s complex, it’s hacky.

I can’t stress how complex and hard to use. The async patterns are numerous and they are nothing like what you could do (easier) with python built-in (async + yield + future) or other libraries.

It’s no joke to upgrade either, it’s like python 2 to python 3, on top of python 2 to python 3.

Nowadays and due to its nature, twisted is only left in relatively complex and relatively old distributed systems projects. It’s not something you’re likely to see a lot of. I bet many were left to die between twisted and python 2-3, it’s too much to keep working.

I have personally found nasty bugs in twisted, workaround for bugs in the Linux kernel 10 years before that crashed it on later Linux, bugs with handling and reusing connections breaking HTTP requests, had one heisenbug I’ve spent days looking into -tracking it down with 99% certainty into twisted- yet unable to pinpoint the cause and write a patch.

Note: I have found bugs in all the libraries mentioned above (some I had to fix, some had a fix in a later version). It’s “normal” to find bugs everywhere but preferably not too breaking and not too often.

How you think you can handle twisted as a 10x developer

How twisted will handle you

Django

Quick note on Django that should be in the top 5 of python web frameworks.

I don’t have much experience with it but if somebody comments on their experience, I can add it here. Django is generally praised for its ORM, magically interfacing the application with the SQL database. I suppose it’s relevant if you make a regular web site backed by a SQL database.

Conclusion

Tornado = GREAT.

Bottle = GREAT. #FlaskDoneRight

Flask = MEDIOCRE. Not indicated for more than one script with few endpoints. Better use bottle or tornado.

Twisted = BAD.

Django = ?

If your application/service can benefit from using workers, HTTP compression, asynchronous subrequests or many other things -only provided by Tornado- you’re better off starting with tornado.

One thought on “My Experience In Production with: Flask, Bottle, Tornado and Twisted

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s