|
|
Subscribe / Log in / New account

Mucking about with microframeworks

By Jake Edge
July 9, 2019

Python does not lack for web frameworks, from all-encompassing frameworks like Django to "nanoframeworks" such as WebCore. A recent "spare time" project caused me to look into options in the middle of this range of choices, which is where the Python "microframeworks" live. In particular, I tried out the Bottle and Flask microframeworks—and learned a lot in the process.

I have some experience working with Python for the web, starting with the Quixote framework that we use here at LWN. I have also done some playing with Django along the way. Neither of those seemed quite right for this latest toy web application. Plus I had heard some good things about Bottle and Flask at various PyCons over the last few years, so it seemed worth an investigation.

Web applications have lots of different parts: form handling, HTML template processing, session management, database access, authentication, internationalization, and so on. Frameworks provide solutions for some or all of those parts. The nano-to-micro-to-full-blown spectrum is defined (loosely, at least) based on how much of this functionality a given framework provides or has opinions about. Most frameworks at any level will allow plugging in different parts, based on the needs of the application and its developers, but nanoframeworks provide little beyond request and response handling, while full-blown frameworks provide an entire stack by default. That stack handles most or all of what a web application requires.

The list of web frameworks on the Python wiki is rather eye-opening. It gives a good idea of the diversity of frameworks, what they provide, what other packages they connect to or use, as well as some idea of how full-blown (or "full-stack" on the wiki page) they are. It seems clear that there is something for everyone out there—and that's just for Python. Other languages undoubtedly have their own sets of frameworks (e.g. Ruby on Rails).

Drinking the WSGI

Modern Python web applications are typically invoked using the Web Server Gateway Interface (WSGI, pronounced "whiskey"). It came out of an effort to have a common web interface instead of the many choices that faced users in the early days (e.g. Common Gateway Interface (CGI) and friends, mod_python). WSGI was first specified in PEP 333 ("Python Web Server Gateway Interface v1.0") in 2003 and was updated in 2010 to version 1.0.1 in PEP 3333, which added various improvements including better Python 3 support. At this point, it seems safe to say that WSGI has caught on; both Bottle and Flask use it (as does Django and it is supported by Quixote as well).

At its most basic, a WSGI application simply provides a way for the web server to call a function with two parameters every time it gets a request from a client:

    result = application(environ, start_response)
environ is a dictionary containing the CGI-style environment variables (e.g. HTTP_USER_AGENT, REMOTE_ADDR, REQUEST_METHOD) along with some wsgi.* values. The start_response() parameter is a function to be called by the application to pass the HTTP status (e.g. "200 OK", "404 Not Found") and a list of tuples with the HTTP response headers (e.g. "Content-type") and values. The application() function returns an iterable yielding zero or more strings of type bytes, which is generally the HTML response to the client.

The Python standard library has the wsgiref module that provides various utilities and a reference implementation of a WSGI server. In just a few lines of code, with no dependencies other than Python itself, one can run a simple WSGI server locally.

Similarly, both Bottle and Flask have the ability to simply run a development web server locally, which uses the application code in much the same way as it will be used on a "real" server. That feature is not uncommon in the web-framework world and it is quite useful. There are various easy ways to debug the code before deploying it using those local servers. The application can then be deployed, using Apache and mod_wsgi, say, to an internet-facing server.

Bottle

One of the nice things about Bottle is that it lacks any dependencies outside of the Python standard library. It can be installed from the Python Package Index (PyPI) using pip or by way of your Linux distribution's package repositories (e.g. dnf install python3-bottle as I did on Fedora). As might be expected, a simple "hello world" example is just that, simple:

    from bottle import route, run, template

    @route('/hello/<name>')
    def index(name):
	return template('<b>Hello {{name}}</b>!', name=name)

    run(host='localhost', port=8080)
Running that sets up a local server that can be accessed via URLs like "http://localhost:8080/hello/world". The route() decorator will send any requests that look like "/hello/XXX" to the index() function, passing anything after the second slash as name.

Bottle uses the SimpleTemplate engine for template handling. As its name implies, it is rather straightforward to use. Double curly braces ("{{" and "}}") enclose substitutions to be made in the text. Those substitutions can be Python expressions that evaluate to something with a string representation:

    {{ name or "amigo" }}
That will substitute name if it has a value (i.e. not None or "") or "amigo" if not. Those substitutions will be HTML escaped in order to avoid cross-site scripting problems, unless the expression starts with a "!", which disables that transformation. Obviously, that feature should be used with great care.

Beyond that, Python code can be embedded in the templates either as a single line that starts with "%" or in a block surrounded by "<%" and "%>". The template() function can be used to render a template as above, or it can be passed a file name:

    return template('hola_template', sub1='foo', sub2='bar', ...)
That will look for a file called views/hola_template.tpl to render; any substitutions should be passed as keyword arguments. The view() decorator can be used instead to render the indicated template based on the dictionary returned from the function:
    @route('/hola')
    @view('hola_template')
    def hola(name='amigo'):
        ...
	return { name=name, sub1='foo', ... }

There is support for using the HTTP request data via a global request object, which provides access mechanisms for various parts of the request such as the request method, form data, cookies, and so on. Likewise, a global response object is used to handle responses sent to the browser.

That covers the bulk of Bottle in a nutshell. Other functionality is available through the plugin interface. There is a list of plugins available, covering things like authentication, Redis integration, using various database systems, session handling, and so forth.

It was quite easy to get started with Bottle and to get quite a ways down the path of implementing my toy application. As I considered further features and directions for it, though, I started to encounter some of the limitations of Bottle. The form handling was fairly rudimentary, though the WTForms form rendering and validation library is mentioned as an option in the Bottle documentation. Beyond that, the largely blank page for the Bottle plugin for the SQLAlchemy database interface and object-relational mapping (ORM) library did not exactly inspire confidence. The latest bottle-sqlalchemy release was from 2015, which is also potentially worrisome.

Many of the limitations of Bottle are intentional, and perfectly reasonable, but as I cast about a bit more, I encountered Miguel Grinberg's Flask Mega-Tutorial, which caused me to look at Flask more closely. Part of my intention with this "project" was to investigate and learn; Grinberg's excellent tutorial makes using Flask even easier than Bottle (which was not particularly hard). I found no equivalent document for Bottle, which may have made all the difference.

Flask

Once I had poked around in the tutorial and the Flask documentation a bit, I decided to see how hard it would be to take the existing Bottle application and move it to Flask. The answer to that was surprising, at least to me. The alterations required were minimal, really, with some changes needed to the templates (by default Flask looks for .html files in the templates directory), call render_template() rather than using template() or @view(), and a little bit of change to the application set-up boilerplate. A Flask "hello world" might look like the following:

    from flask import Flask, render_template_string
    app = Flask(__name__)

    @app.route('/hello/<name>')
    def hello(name):
	return render_template_string('Hello {{name}}', name=name)

While the Bottle "hello world" program could be run directly from the command line to start its development web server, Flask takes a different approach. If the above code were in a file called hw.py, the following command would start the development server:

    $ FLASK_APP="hw" flask run
Note that on Fedora, the Python 3 version of flask is run as flask-3. A .flaskenv file can be populated with the FLASK_APP environment variable (along with the helpful "FLASK_ENV=development" setting for debugging features) so that it does not need to be specified on every run. In development mode, the server notices changes to the application and reloads it, which is rather helpful.

Flask uses the Jinja2 templating language, which shares many features with Bottle's template system, though with some syntax differences. The biggest difference, at least for the fairly simple templates I have been working with, is that statements are enclosed in "{%" and "%}", rather than Bottle's angle-bracket scheme. In truth, I have yet to run into things I couldn't do with either templating system. There are extensions for both frameworks to switch to a different templating language if that is needed or desired.

One nice feature is that templates can inherit from a base template in Flask. That can also be done in Bottle using @view() but it is less convenient—or so it seemed to me. Flask also has direct support for sessions, so values can be stored and retrieved from the object. Flask serializes the session data into a cookie that gets cryptographically signed. That means the session's contents are visible to users, but cannot be modified; it also means that session data needs to fit inside the 4K limit imposed for cookies by most browsers.

The difference between the core functionality of Flask and Bottle is not huge by any means. Either makes a good basis for a simple web application. The main difference between them seems to be embodied in the momentum of the project and its community. Perhaps Bottle is simply mature and has the majority of the features its users and developers are looking for, much like Quixote:

While Quixote is actively maintained, the frequency of releases is low. Existing Quixote users seem happy with the features Quixote provides, generally it just gets out of your way and lets you write application code.

Bottle has fairly frequent releases, but otherwise seems to be standing still. The Twitter feed, blog, mailing list, and GitHub repository have not been updated much recently, for example. Bottle also lacks the "killer tutorial" that Grinberg has put together for Flask. But part of what makes that Flask tutorial so useful is all of the plugins from the Flask community that Grinberg uses along the way.

In some sense, the tutorial takes Flask from a microframework to a full-stack framework (or a long way down that path anyway). It is an opinionated tutorial that picks and chooses various Flask plugins that help implement each chapter's feature for the "microblog" application that he describes. For example, it uses Flask-WTF to interface to WTForms, Flask-SQLAlchemy for an ORM, Flask-Login for user-session management, Flask-Mail for sending email, and so on.

While I haven't (yet, perhaps) needed some of those features, I did confirm that most or all of the packages are available for Fedora, which is convenient for me. In many ways, Grinberg's tutorial "tied the room together" in terms of seeing a simple Flask application growing into something "real". It showed how to add some functionality I wanted to Flask (form handling in particular) and to see how other possible features could also be added easily down the road.

One could perhaps argue that simply starting with a full-stack framework, rather than adding bits piecemeal to get there, might make more sense—and perhaps it does. But those larger frameworks are rather more daunting to get started with and are, obviously, opinionated as well. If I disagree with Grinberg about the need for a particular piece, I can just leave it out or choose something different; that's more difficult to do with, say, Django.

Lots of liquor references

Apparently working with web applications (and frameworks) leads developers to start thinking about whiskey bottles and flasks, or so it would seem based on some of the names. Web programming is a fiddly business in several dimensions. Web frameworks help with some of the server-side fiddly bits, but there are still plenty more available to be tackled.

HTML and CSS are sometimes irritatingly painful to work with and web frameworks can only provide so much help there. At one level, HTML/CSS is a truly amazing technology that is supported by so many different kinds of programs and devices. On another, though, it is an ugly, hard-to-use format with an inadequate set of user-interface controls and mechanisms so that it often seems much harder than it should be to accomplish what you are trying to do.</rant>

But, of course, web programming is fun and you can easily show your friends what silly thing you have been working on, no matter how far away they live. For that, Pythonistas (and perhaps others) should look at the huge diversity of web frameworks available for the language and, if the mood to create that silly thing strikes, give one of them a try. Bottle or Flask might be a great place to start.


Index entries for this article
PythonWeb


to post comments

Mucking about with microframeworks

Posted Jul 10, 2019 0:11 UTC (Wed) by Kamilion (subscriber, #42576) [Link] (2 responses)

Flask's been my go-to for at least five to six years now.

Although, I'd like to point out some of it's best extensions aren't very well known like flask-classy/classful
https://github.com/teracyhq/flask-classful

Can also go the other way, and sit on top of some C code.

Sanic is a drop in async replacement for Flask (more or less) with massive performance gain.
https://github.com/huge-success/sanic

Responder is a really nice tool as well, which can mount existing flask endpoints and handle background threads.
https://python-responder.org/en/latest/tour.html#mount-a-...
https://github.com/kennethreitz/responder
It sits on top of Uvicorn and Starlette ASGI.
https://www.starlette.io/

Borrowing the magic of node's libuv, and the uvloop FFI, python can zoom as fast as Golang can.
https://magic.io/blog/uvloop-blazing-fast-python-networking/
https://www.techempower.com/benchmarks/#section=data-r17&...

Unfortunately, I've been using uwsgi+nginx+uwsgi_pass for ages; and I'm reluctant to move back to proxy_pass, so I've stuck to pure Flask for now.

I could add Responder or Sanic to my existing webapps with a snap, more or less, I'd have to change out this singular python object, from Flask's implementation to Sanic's (which is API compatible)

flask_core = Flask(__name__)
becomes:
flask_core = Sanic(__name__)

api = responder.API()
api.mount('/yeflask', flask_core)

But don't blame me if ye cannot get ye flask.
https://tvtropes.org/pmwiki/pmwiki.php/Main/YouCantGetYeF...

Mucking about with microframeworks

Posted Jul 11, 2019 13:36 UTC (Thu) by hazmat (subscriber, #668) [Link]

the chalice, flask, bottle etc references are more takes on python's association to monthy python, and i think derive from tangential humor associations to the quest for the holy grail ;-)

Mucking about with microframeworks

Posted Jul 11, 2019 19:05 UTC (Thu) by rillian (subscriber, #11344) [Link]

Thanks for the article and the additional references. I've been working with a flask app and the extra tutorial resources have helped a lot. flask-classful seems really cool; I'm surprised I haven't seen more of it.

I'd also come across projects like FastAPI and uvicorn, but have had trouble understanding where they sit in the ecosystem. They talk about being really fast because async, but the examples are all just serving "Hello, World" over json. I couldn't find any guides about which framework to choose for different applications.

Mucking about with microframeworks

Posted Jul 10, 2019 0:58 UTC (Wed) by pablotron (subscriber, #105494) [Link] (22 responses)

It's also worth noting that the recommended way of installing Flask and extensions is in a virtual environment (e.g. virtualenv or pipenv), rather than via system packages (dnf, apt, etc).

From the Flask documentation:

Use a virtual environment to manage the dependencies for your project, both in development and in production.

What problem does a virtual environment solve? The more Python projects you have, the more likely it is that you need to work with different versions of Python libraries, or even Python itself. Newer versions of libraries for one project can break compatibility in another project.

Virtual environments are independent groups of Python libraries, one for each project. Packages installed for one project will not affect other projects or the operating system’s packages.

Mucking about with microframeworks

Posted Jul 10, 2019 15:17 UTC (Wed) by NYKevin (subscriber, #129325) [Link] (7 responses)

This is standard for Python development in general. The alternatives are (in descending order of reasonableness) installing dependencies per-user (but then you can't have different versions of the same dep under the same user, so now you need a user for each app), installing them globally with the system package manager (no pip freeze etc. functionality, no multiple installation, and you may or may not have sufficiently up-to-date system packages), or installing them into /usr/local/... with pip (eww). You could also use containers, but that's a rather "heavy" solution, which may not appeal to you if you're using a microframework.

Mucking about with microframeworks

Posted Jul 11, 2019 14:31 UTC (Thu) by mirabilos (subscriber, #84359) [Link] (5 responses)

“but then you can't have different versions of the same dep” but this is a good thing, so the correct way is to install system-wide

Mucking about with microframeworks

Posted Jul 11, 2019 22:01 UTC (Thu) by smitty_one_each (subscriber, #28989) [Link] (3 responses)

Disagree.
app_a is humming along, and uses a component.
I start app_b and install the same component, and app_b upgrades a dependency that breaks app_a.
This will occur or be discovered at the least opportune time.

Mucking about with microframeworks

Posted Jul 11, 2019 23:21 UTC (Thu) by mirabilos (subscriber, #84359) [Link] (1 responses)

In that case, complain to the distro packager of app_b.

Mucking about with microframeworks

Posted Jul 12, 2019 0:50 UTC (Fri) by smitty_one_each (subscriber, #28989) [Link]

If one is using apt/pacman/yum, then pip is irrelevant.

And that is sensible if one is merely installing some python for the infrastructure case, e.g. Ansible.

If doing any substantial tinkering, the distro is going to be a pain point.

Mucking about with microframeworks

Posted Jul 15, 2019 19:32 UTC (Mon) by mathstuf (subscriber, #69389) [Link]

Ideally, `app_a` would have an upper bound which would have caused a conflict in the system package manager too. Or you go and tsk-tsk the library developers for releasing a non-semver-adhering change.

Mucking about with microframeworks

Posted Jul 12, 2019 7:07 UTC (Fri) by massimiliano (subscriber, #3048) [Link]

For me this quickly became install system-wide in a container.

It's the "general way" of having project-specific environments (regardless of the language or framework used in each project) and keeping my development workstation clean.

Moreover, these days I'm going to deploy containers anyway, so developing inside containers effectively unifies local and "remote" (or "cloud") build environments.

Mucking about with microframeworks

Posted Jul 16, 2019 9:41 UTC (Tue) by gdamjan (subscriber, #33634) [Link]

> The alternatives are (in descending order of reasonableness) installing dependencies per-user (but then you can't have different versions of the same dep under the same user

you can (have more versions per single user), see the PYTHONUSERBASE environment variable[1]. it''s basically a single env var virtualenv, built-in into python with no need for the virtualenv hacks.

reference:
[1] https://www.python.org/dev/peps/pep-0370/

Mucking about with microframeworks

Posted Jul 10, 2019 19:44 UTC (Wed) by Sesse (subscriber, #53779) [Link] (13 responses)

The more Python modules you have, the nicer it is to have them all security-updated in one central place (your distribution) as opposed to once per project (venv) you might have… Not to mention what happens when you need a security update and can only solve that by upgrading to a new version, with backwards compatibility ripple effects throughout the venv.

Mucking about with microframeworks

Posted Jul 10, 2019 20:21 UTC (Wed) by Cyberax (✭ supporter ✭, #52523) [Link] (9 responses)

Unless you need a newer version that is not packaged by your distro.

Mucking about with microframeworks

Posted Jul 11, 2019 9:30 UTC (Thu) by smurf (subscriber, #17840) [Link] (2 responses)

So you package it yourself and upload to your site-local archive. For most Python modules, this process anything but rocket science.

Mucking about with microframeworks

Posted Jul 11, 2019 9:56 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link] (1 responses)

I honestly have no idea how to do this, and how to manage a local package archive. Ubuntu PPAs were the closest to that.

Quick googling also doesn't fill me with confidence about the ease of doing it.

Mucking about with microframeworks

Posted Jul 20, 2019 19:28 UTC (Sat) by garloff (subscriber, #319) [Link]

Open Build Service is a very efficient way to get it done.
https://build.opensuse.org/
Before you dismiss b/c it sounds like it's SUSE focused: It builds RPMs and DEBs for a large set of Linux distros (and many architectures, though most Py packages are noarch).

Mucking about with microframeworks

Posted Jul 11, 2019 14:32 UTC (Thu) by mirabilos (subscriber, #84359) [Link] (5 responses)

Just target Debian stable and do with the older versions. You don’t really need that new feature right now, and you’ll get all the security fixes still.

Mucking about with microframeworks

Posted Jul 11, 2019 14:48 UTC (Thu) by rahulsundaram (subscriber, #21946) [Link] (2 responses)

>You don’t really need that new feature right now

How did you get to conclude that?

Mucking about with microframeworks

Posted Jul 11, 2019 15:03 UTC (Thu) by mirabilos (subscriber, #84359) [Link] (1 responses)

You don’t. You’re creating your own new software, and thus you can program it in a way that it does not depend on that shiny new feature not available in stable yet. (Or simply embed a backport in your own code, added when run on older stable distros.)

Mucking about with microframeworks

Posted Jul 11, 2019 15:19 UTC (Thu) by rahulsundaram (subscriber, #21946) [Link]

>. You’re creating your own new software, and thus you can program it in a way that it does not depend on that shiny new feature not available in stable yet.

You are asking developers to limit themselves to match a single slow moving distribution's schedule. History has clearly shown us that it is not going to work. Distributions will simply get bypassed

Mucking about with microframeworks

Posted Jul 11, 2019 18:37 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link] (1 responses)

Yeah, a great advice. I'm starting to think that the Windows approach (no package manager whatsoever) is actually the superior one.

Mucking about with microframeworks

Posted Jul 11, 2019 19:54 UTC (Thu) by rodgerd (guest, #58896) [Link]

The approach of a slow-moving, stable OS layer, with a high rate of change applications does seem to be what users want, with a mechanism for app discovery (Apple App Store, Windows Store, etc). This is what SuSE (OBS) and Canonical and Fedora (their respective Flatpack etc) seem to be trying to emulate.

Mucking about with microframeworks

Posted Jul 11, 2019 17:12 UTC (Thu) by patrakov (subscriber, #97174) [Link] (2 responses)

This would be true if the distributions actually provided security updates for security issues with Python modules. Right now the model seems to be "no CVE, no update", even if a minor release is marked as security-relevant by the authors. Let me quote an email that I sent to the maintainer and then later forwarded to security@debian.org, with no response and no updated package in Debian Stretch (except in backports).

-------- Forwarded Message --------
Subject: Regarding python-jinja2 in Debian Stable
Date: Tue, 16 Oct 2018 04:35:31 +0500
From: Alexander E. Patrakov <patrakov@gmail.com>
To: piotr@debian.org

Hello,

you are listed as a maintainer of python-jinja2.

Today I was reading the blog of Armin Ronacher (the upstream author of
the said package), and found these two entries from year 2016:

http://lucumr.pocoo.org/2016/12/29/careful-with-str-format/
https://palletsprojects.com/blog/jinja-281-released/

I.e., Jinja 2.8.1 is a security release, for a sandbox escape via a
crafted template, with an exploit readily available and upstream
admitting that attacker-controlled templates do sometimes happen.

There is no CVE ID for this vulnerability.

Debian Stable is still at 2.8-1, i.e. does not include any patches over
the vulnerable upstream 2.8 release. Could you please investigate
whether it makes sense to include the security fix?

Mucking about with microframeworks

Posted Jul 11, 2019 17:24 UTC (Thu) by zdzichu (subscriber, #17118) [Link]

Please, what Debian is doing is NOT "what distributions are doing". There are other distribution with much more reasonable update policies.

Mucking about with microframeworks

Posted Jul 12, 2019 8:07 UTC (Fri) by pabs (subscriber, #43278) [Link]

Apparently that issue does have a CVE:

https://security-tracker.debian.org/tracker/CVE-2016-10745

It is marked as not going to get an update by the Debian security team due to being a minor issue. The maintainer or anyone else can do an update for the issue:

https://www.debian.org/doc/manuals/developers-reference/c...
https://www.debian.org/doc/manuals/developers-reference/c...

Mucking about with microframeworks

Posted Jul 10, 2019 6:58 UTC (Wed) by smurf (subscriber, #17840) [Link]

There's also "Quart", which has the same API as Flask except fully async-ized (or, even better, trio-ized if you use "quart_trio"), which is helpful e.g. if you need many parallel WebSocket sessions and don't particularly want 10k threads.

WebCore is not suitable for production

Posted Jul 10, 2019 12:19 UTC (Wed) by mirabilos (subscriber, #84359) [Link]

From their website:

> Note: We strongly recommend always using a container, virtualization, or sandboxing environment of some kind when developing using Python; installing things system-wide is yucky (for a variety of reasons) nine times out of ten. We prefer light-weight virtualenv, others prefer solutions as robust as Vagrant.

This reads as “we don’t want to be packaged in a distro and thus will break things willy-nilly to ensure they don’t do that”.

Mucking about with microframeworks

Posted Jul 10, 2019 13:30 UTC (Wed) by pj (subscriber, #4506) [Link]

Newer than WSGI is ASGI, which is essentially the async version, which can thus support websockets, something that WSGI, by nature, is unable to do.


Copyright © 2019, Eklektix, Inc.
This article may be redistributed under the terms of the Creative Commons CC BY-SA 4.0 license
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds