Web Frameworks - Step By Step

This page is a bluffer's guide to the pieces that need to be put together to create an application using a python web framework. Regardless of the framework, the steps required to put together a basic application can be quite similar. The intention is to compare and contrast these common steps rather than to suggest which framework is best for any given task. If you already know one of the frameworks listed below then this guide might help you put the other frameworks into perspective.

Each framework mentioned below has its own unique characteristics and it is recommended that you follow the excellent tutorials available for them.

The web frameworks compared so far are:

The plan is to add more to this list!

1. Mapping a request for a URL to a piece of code

So the user enters a URL in the browser's address bar. How does the request for that URL get mapped to the right bit of python code on the web server?

Google App Engine

The main application configuration file for a Google App Engine application is the app.yaml file found in the application root directory. The handlers section of the file maps a URL to a python script using a regular expression.

For example, the following maps all URLs (denoted by /.*) to the helloworld.py python script:

application: test
version: 1
runtime: python
api_version: 1

handlers:
- url: /.*
  script: helloworld.py

A further mapping in helloworld.py maps the actual URL to a python class extending google.appengine.ext.webapp.RequestHandler is responsible for handling the request. For example, the following maps the URLs / and /sign to the classes MainPage and Guestbook respectively:

import wsgiref
from google.appengine.ext import webapp

class MainPage(webapp.RequestHandler):
    ...

class Guestbook(webapp.RequestHandler):
    ...

def main():
    application = webapp.WSGIApplication([ ('/', MainPage),
                                           ('/sign', Guestbook) ])
    wsgiref.handlers.CGIHandler().run(application)

if __name__ == "__main__":
    main()

Django

Django's URL mapping takes place by convention in the urls.py python file. This file contains the variable urlpatterns which is a python list of patterns describing the mappings. Unlike Google App Engine which ultimately maps a URL to a python class, Django maps a URL to a function. Again, a regular expression is used to map the URL.

For example, the following maps the URL /helloworld to the function helloworld in module mysite.myapp.views:

from django.conf.urls.defaults import *

urlpatterns = patterns('mysite.myapp.views',
        (r'helloworld/$', 'helloworld'),
)

Note that the function handling the request is by convention found in the views.py file.

web.py

URL mapping in web.py maps a URL directly to a python class using a regular expression. (Compare this with Google App Engine which maps a regular expression to a python script file which in turn maps a regular expression to a python class). The mapping is a simple tuple which is then used to create a web application.

import web

urls = ("/.*", "Hello")
app = web.application(urls, globals())

...

if __name__ == "__main__":
    app.run()

2. Handling the request

By now, the browser has requested a URL and the framework has chosen which bit of python code to execute. So what does the python code which handles this request look like?

Google App Engine

We have already seen that a class extending webapp.RequestHandler is defined to respond to a URL, but what exactly is executed? The answer is different depending on whether the request is a GET or a POST:

A GET request, for example, a request made by entering a URL into the browser address bar is handled with the get method. A POST request, commonly preformed by the browser as a result of form submission, is handled with the post method.

Parameters passed along with the request are accessed from the property self.request found in the parent webapp.RequestHandler.

For example, a class that handles both methods might be defined as follows:

class MainPage(webapp.RequestHandler):
    def get(self):
        ...

    def post(self):
        content = cgi.escape(self.request.get('content')))
        ...

Note how the parameter content retrieved from the POST request is escaped (or 'sanitised') to remove any potentially malicious HTML content.

Django

We have shown that a simple python function handles a request. Unlike Google App Engine, the same function is called regardless of whether it is a GET or a POST. Parameters passed along with the request are accessed from HttpRequest object passed as an argument to the function. The following handles a POST request retrieving the content parameter.

def helloworld(request):
    content = request.POST['content']

If you need to know what type of request you're dealing with, you can use the following:

if request.method == 'GET':
    do_something()
elif request.method == 'POST':
    do_something_else()

Note that Django shuns some use of GET parameters in favour of 'beautiful URL design' and the principle that Cool URIs Don't Change. Django provides a way of parsing the URL directly and passing elements to the view function. For example, compare the following URLs and corresponding view funtions:

# http://www.myblog.com/2008/01/01/
def helloworld(request, year, month, day):
    ...
# http://www.myblog.com?year=2008&month=01&day=01
def helloworld(request):
    year = request.GET['year']
    month = request.GET['month']
    day = request.GET['day']
    ...

web.py

Similar to Google App Engine, an instance of a python class handles a request. Unlike Google App Engine, the class doesn't have to inherit from any particular python class. To handle a GET request, the class must contain a function called GET, and similarly a function called POST for a POST request. These are called with the match groups from the URL regexp which matched.

import web

urls = ("/(.*)", "Hello")   # note the match group
app = web.application(urls, globals())

class Hello:
    def GET(self, name):
        ...

if __name__ == "__main__":
    app.run()

3. Returning a response to the user

Our python code has now received a request along with any parameters associated with that request. How do we return a response to the user, typically, an HTML web page?

Google App Engine

The response is returned to the user by writing to the property self.response found in the parent webapp.RequestHandler class. Here is an example that shows both a GET and POST request (with parameters) returning an HTML page to the user.

import wsgiref, cgi
from google.appengine.ext import webapp

class MainPage(webapp.RequestHandler):
    def get(self):
        self.response.out.write('<html><body>Hello, World!</body><html>')

class Guestbook(webapp.RequestHandler):
    def post(self):
        self.response.headers['Content-Type'] = 'text/html'
        self.response.out.write('<html><body>You wrote:<pre>')
        self.response.out.write(cgi.escape(self.request.get('content')))
        self.response.out.write('</pre></body></html>')

Django

The response is returned to the user by returning an HttpResponse object from the function handling the request as follows:

def helloworld(request):
    content = request.GET['content']
    return HttpResponse('<html><body>You wrote:<pre>%s</pre></body></html>' % cgi.escape(content))

web.py

Returning a response to the user is simply a matter of returning a string from the GET method.

import web

urls = ("/.*", "hello")
app = web.application(urls, globals())

class hello:
    def GET(self):
        return 'Hello, world!'

if __name__ == "__main__":
    app.run()

4. Modelling, storing and accessing data for use by the webapp

Google App Engine

Model definition and model implementation are one and the same i.e. the definition of an entity is a python class, and that class is used to instantiate those entities.

Although a "schema" is defined, further properties may be added at will and persisted on an object by object basis (by use of Expando?). This is achievable through Google's proprietary datastore implementation.

Django

Similar to Google App Engine, model definition and implementation are the python class file where the class extends db.Model. Arbitrary properties not part of the original schema may be added to objects at runtime, however in contrast to Google App Engine, these properties are not persisted, simply by virtue of the fact that persistence is to a relational database where each property is mapped to a database field.

web.py

web.py employs a more straightforward implementation compared to Google App Engine and Django in that there is no data modelling in python code, rather all data definition takes place entirely in the database through the database table definitions. Data is mapped to python code dynamically whereby arbitrary objects who's attribute names map to database column names may be read or inserted into the database. Examples follow:

Setup:

web.config.db_parameters = dict(dbn='postgres', user='username', pw='password', db='dbname')

Table definition:

CREATE TABLE person (
  name varchar(32),
  age int);

Insert rows:

INSERT INTO person (name, age) VALUES ('Bobo', 55);
INSERT INTO person (name, age) VALUES ('Momo', 27);

Read sequence of persons from database:

def GET(self):
    # Returns rows from database whereby
    # persons[0].name = 'Bobo', persons[0].age = 55
    # persons[1].name = 'Momo', persons[1].age = 27
    persons = web.select('person')
    ...

5. Style considerations

Google App Engine

The online tutorial seems to favour bundling everything into a single file. I have yet to explore this further.

Django

Django expects models, views, urls to appear in their own modules. This is the principle of convention over configuration, though this can be overridden.

web.py

The tutorial uses the built-in templetor templating engine. This provides a web.template.render function that turns a directory of template files into an object with one method per file, as in:

render = web.template.render('templates/')
print render.index(name=name)  # renders templates/index

web.py doesn't enforce any arrangement of code, and using any part (such as templetor or the database support) is optional.