Mucking about with microframeworks
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:
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 | |
|---|---|
| Python | Web |