Over various pints of beer, emails, and late-night twitter tweets, I’ve alone and with others wondered about whether a smart, well-adjusted programmer would use Pylons or CherryPy for all his web programming needs (and whether such a programmer would take the time to convert from CherryPy to Pylons). Pylons is newish to me, but I’ve been using CherryPy (on and off) for years now. What troubles me about CherryPy is that despite all those years of experience, there are still parts of CherryPy I struggle with (and not just this horrible while-true-except-pass loop). Here are my partially collected thoughts. I’ll start with what bugs me enough about CherryPy for me to seek alternatives.
If you visit the URI /foo/bar/spam, CherryPy looks for a foo.bar controller with a spam action (sort of). There are other mostly irrelevant complexities (mount points for quasi-apps and such), but that’s the gist. You can write your own “dispatcher” in CherryPy to map URLs to executable code, or use ones such as the RoutesDispatcher, which uses Groovie Routes to map URLs to code. But using your own component is always kind of a sour-tasting afterthought in CherryPy and you’re sometimes reminded of that, unpleasantly (for example, routes for static content do not work in RoutesDispatcher).
In version 3, CherryPy’s previously simple configuration system got kind of confusing. I started writing a lot of code like this:
cherrypy.config.update(myConfig) app = cherrypy.tree.mount(None, moreConfig) cherrypy.config.update(someFile) cherrypy.quickstart(app)
CherryPy lets you pass in configuration options a number of ways: In Python: dicts passed to config.update, dicts passed to tree.mount, or dicts on your controller object. In ConfigParser syntax: as filenames passed to config.update or tree.mount. The problem is that I’m always left with trial-and-error in figuring out what options I have; configuration directives don’t work everywhere, and you’re given relatively little guidance finding out where. If the documentation were better, that might help, but it’s confusing no matter what.
Which brings me to my most significant grievance with CherryPy: Its documentation is mediocre and because it takes such a novel approach to so many things, it needs good documentation. Most of CherryPy’s documentation is on a loosely organized wiki that resembles someone’s lecture notes more than it resembles documentation. This makes finding specific pieces of information difficult; for example, the RoutesDispatcher from above is buried deep down on a page called “PageHandlers” and is otherwise mostly unmentioned. Sometimes the wiki looks helpful, such as on this file upload page, but in practice the examples given are apparently built for an old version of the framework (“from cherrypy.lib.filter import basefilter” results in an ImportError).
You can always ask CherryPy’s mailing list, but be forewarned: it’s not very inhabited now that TurboGears moved to Pylons.
Where is CherryPy superior?
After all these gripes, you might think I’m fully convinced that move to Pylons is good. A simple “Hello World” comparison reveals why that’s not the case. In CherryPy:
import cherrypy class HelloWorld(object): def index(self): return "Hello World!" index.exposed = True cherrypy.quickstart(HelloWorld())
In Pylons: … this is going to take some time. Pylons doesn’t let you create a Hello World app in one file. Or even one directory. You start by running this command (for convenience and comic effect, there are scroll bars):
kkinder@kkinder-laptop ~> paster create MyHelloWorldProject -t pylons
Selected and implied templates:
Pylons#pylons Pylons application template
Variables:
egg: MyHelloWorldProject
package: myhelloworldproject
project: MyHelloWorldProject
Enter template_engine (mako/genshi/jinja2/etc: Template language) ['mako']:
Enter sqlalchemy (True/False: Include SQLAlchemy 0.5 configuration) [False]:
Creating template pylons
Creating directory ./MyHelloWorldProject
Recursing into +package+
Creating ./MyHelloWorldProject/myhelloworldproject/
Copying templates/default_project/+package+/__init__.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/__init__.py
Recursing into config
Creating ./MyHelloWorldProject/myhelloworldproject/config/
Copying templates/default_project/+package+/config/__init__.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/config/__init__.py
Copying templates/default_project/+package+/config/deployment.ini_tmpl_tmpl to ./MyHelloWorldProject/myhelloworldproject/config/deployment.ini_tmpl
Copying templates/default_project/+package+/config/environment.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/config/environment.py
Copying templates/default_project/+package+/config/middleware.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/config/middleware.py
Copying templates/default_project/+package+/config/routing.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/config/routing.py
Recursing into controllers
Creating ./MyHelloWorldProject/myhelloworldproject/controllers/
Copying templates/default_project/+package+/controllers/__init__.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/controllers/__init__.py
Copying templates/default_project/+package+/controllers/error.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/controllers/error.py
Recursing into lib
Creating ./MyHelloWorldProject/myhelloworldproject/lib/
Copying templates/default_project/+package+/lib/__init__.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/lib/__init__.py
Copying templates/default_project/+package+/lib/app_globals.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/lib/app_globals.py
Copying templates/default_project/+package+/lib/base.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/lib/base.py
Copying templates/default_project/+package+/lib/helpers.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/lib/helpers.py
Recursing into model
Creating ./MyHelloWorldProject/myhelloworldproject/model/
Copying templates/default_project/+package+/model/__init__.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/model/__init__.py
Recursing into public
Creating ./MyHelloWorldProject/myhelloworldproject/public/
Copying templates/default_project/+package+/public/bg.png to ./MyHelloWorldProject/myhelloworldproject/public/bg.png
Copying templates/default_project/+package+/public/favicon.ico to ./MyHelloWorldProject/myhelloworldproject/public/favicon.ico
Copying templates/default_project/+package+/public/index.html_tmpl to ./MyHelloWorldProject/myhelloworldproject/public/index.html
Copying templates/default_project/+package+/public/pylons-logo.gif to ./MyHelloWorldProject/myhelloworldproject/public/pylons-logo.gif
Recursing into templates
Creating ./MyHelloWorldProject/myhelloworldproject/templates/
Recursing into tests
Creating ./MyHelloWorldProject/myhelloworldproject/tests/
Copying templates/default_project/+package+/tests/__init__.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/tests/__init__.py
Recursing into functional
Creating ./MyHelloWorldProject/myhelloworldproject/tests/functional/
Copying templates/default_project/+package+/tests/functional/__init__.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/tests/functional/__init__.py
Copying templates/default_project/+package+/tests/test_models.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/tests/test_models.py
Copying templates/default_project/+package+/websetup.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/websetup.py
Copying templates/default_project/MANIFEST.in_tmpl to ./MyHelloWorldProject/MANIFEST.in
Copying templates/default_project/README.txt_tmpl to ./MyHelloWorldProject/README.txt
Copying templates/default_project/development.ini_tmpl to ./MyHelloWorldProject/development.ini
Recursing into docs
Creating ./MyHelloWorldProject/docs/
Copying templates/default_project/docs/index.txt_tmpl to ./MyHelloWorldProject/docs/index.txt
Copying templates/default_project/ez_setup.py to ./MyHelloWorldProject/ez_setup.py
Copying templates/default_project/setup.cfg_tmpl to ./MyHelloWorldProject/setup.cfg
Copying templates/default_project/setup.py_tmpl to ./MyHelloWorldProject/setup.py
Copying templates/default_project/test.ini_tmpl to ./MyHelloWorldProject/test.ini
Running /usr/bin/python setup.py egg_info
I of course went for the lighter option that doesn’t include SQLAlchemy. Okay, we’re almost there. Now I’m going to create a Hello controller:
kkinder@kkinder-laptop ~/MyHelloWorldProject> paster controller Hello Creating /home/kkinder/MyHelloWorldProject/myhelloworldproject/controllers/Hello.py Creating /home/kkinder/MyHelloWorldProject/myhelloworldproject/tests/functional/test_Hello.py
This is where Pylons READS MY MIND and does the work for me. Take a look at the new Hello.py controller it made for me:
import logging from pylons import request, response, session, tmpl_context as c from pylons.controllers.util import abort, redirect_to from myhelloworldproject.lib.base import BaseController, render log = logging.getLogger(__name__) class HelloController(BaseController): def index(self): # Return a rendered template #return render('/Hello.mako') # or, return a response return 'Hello World'
But I’m not done yet. Now I need to edit my routing.py file, check my development.ini for any settings, run “setup.py develop” and then use “paster serve” to finally test my app. By the time I’m done printing “Hello World”, I have 42 files in 11 directories:
. |-- MANIFEST.in |-- MyHelloWorldProject.egg-info | |-- PKG-INFO | |-- SOURCES.txt | |-- dependency_links.txt | |-- entry_points.txt | |-- not-zip-safe | |-- paster_plugins.txt | |-- requires.txt | `-- top_level.txt |-- README.txt |-- development.ini |-- docs | `-- index.txt |-- ez_setup.py |-- myhelloworldproject | |-- __init__.py | |-- __init__.pyc | |-- config | | |-- __init__.py | | |-- deployment.ini_tmpl | | |-- environment.py | | |-- middleware.py | | `-- routing.py | |-- controllers | | |-- Hello.py | | |-- __init__.py | | `-- error.py | |-- lib | | |-- __init__.py | | |-- __init__.pyc | | |-- app_globals.py | | |-- base.py | | |-- base.pyc | | `-- helpers.py | |-- model | | `-- __init__.py | |-- public | | |-- bg.png | | |-- favicon.ico | | |-- index.html | | `-- pylons-logo.gif | |-- templates | |-- tests | | |-- __init__.py | | |-- functional | | | |-- __init__.py | | | `-- test_Hello.py | | `-- test_models.py | `-- websetup.py |-- setup.cfg |-- setup.py `-- test.ini 11 directories, 42 files
The Django/Pylons/TurboGears frameworks all follow a teeth gnashing design pattern that came out of Java and was made ugly in Ruby: Code generation. This is just my opinion, but in a language as dynamic and meta-programmative as Python, code generation is obsolete at best. What happens when the basic layout for a project changes a bit in the next version of Pylons? Do I “diff” the paste template and compare it to the old version to see what changed? And half of the configuration is in Pylon’s WSGI-obsessed middleware.py file, which is itself both a product of code generation and subsequent edits by the developer:
"""Pylons middleware initialization""" from beaker.middleware import CacheMiddleware, SessionMiddleware from paste.cascade import Cascade from paste.registry import RegistryManager from paste.urlparser import StaticURLParser from paste.deploy.converters import asbool from pylons import config from pylons.middleware import ErrorHandler, StatusCodeRedirect from pylons.wsgiapp import PylonsApp from routes.middleware import RoutesMiddleware from myhelloworldproject.config.environment import load_environment def make_app(global_conf, full_stack=True, static_files=True, **app_conf): """Create a Pylons WSGI application and return it ``global_conf`` The inherited configuration for this application. Normally from the [DEFAULT] section of the Paste ini file. ``full_stack`` Whether this application provides a full WSGI stack (by default, meaning it handles its own exceptions and errors). Disable full_stack when this application is "managed" by another WSGI middleware. ``static_files`` Whether this application serves its own static files; disable when another web server is responsible for serving them. ``app_conf`` The application's local configuration. Normally specified in the [app:<name>] section of the Paste ini file (where <name> defaults to main). """ # Configure the Pylons environment load_environment(global_conf, app_conf) # The Pylons WSGI app app = PylonsApp() # Routing/Session/Cache Middleware app = RoutesMiddleware(app, config['routes.map']) app = SessionMiddleware(app, config) app = CacheMiddleware(app, config) # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares) if asbool(full_stack): # Handle Python exceptions app = ErrorHandler(app, global_conf, **config['pylons.errorware']) # Display error documents for 401, 403, 404 status codes (and # 500 when debug is disabled) if asbool(config['debug']): app = StatusCodeRedirect(app) else: app = StatusCodeRedirect(app, [400, 401, 403, 404, 500]) # Establish the Registry for this application app = RegistryManager(app) if asbool(static_files): # Serve static files static_app = StaticURLParser(config['pylons.paths']['static_files']) app = Cascade([static_app, app]) return app
Beyond its sanity in not generating piles of code, CherryPy has other advantages. Its testing framework is very easy to use, but and also documented with almost no examples. Pylons, in contrast, tells you where to put the tests but doesn’t make it quite as easy to run them.
On a day to day basis, writing code in CherryPy is seldom a chore; it isn’t exciting or cutting edge, but it’s simple and usually CherryPy does a good job of staying out of your way. I’m never left wondering in CherryPy whether there is a specific “CherryPy way to do it,” while in Pylons, my framework even has an opinion on what Javascript library I choose (it likes script.aculo.us, although it doesn’t bang you over the head with it). CherryPy is usually simpler: take a look at repoze.who/what or AuthKit in the Pylons world and compare a very simple CherryPy auth example.
But there are places where Pylons is amazing…
One more thing: What’s missing from CherryPy?
Pylons error handling is the one killer feature it has that CherryPy just lacks. Skip to line 598 of the “Hello, World” pylons project and try throwing an error:
def index(self): raise ValueError
Then use the controller. In CherryPy, I would get a Python traceback wrapped in a <pre> tag. In Pylons, I get an ajax debugger with expression evaluation and variable inspection:
I can inspect variables and evaluate expressions at each point in the traceback:
Along with view environment info, complete source code of running file and template, and more.
Wrapping it up
I normally would have a strong aversion to “glue” frameworks that try (usually badly) to integrate unrelated toolsets. Having said that, when I sit down and build my own framework in CherryPy, I want to use SQLAlchemy, Groovie Routes, and Mako templates. (Pylons’ “webhelpers” are nice too.)
Pylons offers all those those tools in a bundle. Also, its mailing list is far more active. While CherryPy manages a post or two a day, Pylons has a high quality post every hour or more. I’m not saying I’m giving up on CherryPy, but if I started a project today, I would seriously consider Pylons as a more friendly alternative, even if it relies on code generation.


Thanks, Ken. Always good to get feedback so we can improve.
Question for ya: you mentioned that the CherryPy RoutesDispatcher documentation is sort of in the wrong place. What would be the right place in your estimation?
Hey Robert, thanks for reading and replying to my blog post. What strikes me is that the Table of Contents is built with an existing knowledge of how CherryPy works:
http://www.cherrypy.org/wiki/TableOfContents
For example, it makes sense that url mapping is part of the request cycle, but I have to know ahead of time that CherryPy calls the objects that map URLs to controllers, “dispatchers.” And shouldn’t ReturnVsYield be in the “request cycle” too?
I’d go for more of a generally worded, more structured table of contents with headings like Basic Development that would cover Controllers, cookies, and the like. Then another section for extending CherryPy. Then maybe another on deploying, under which BehindApache, WSGI, etc would be placed.
Also, I would use full phrases as headings, not just wiki stubs. Take a look at the Django or Pylons documentation and it’s broken up by version and purpose. Not that CherryPy’w wiki approach doesn’t have advantages, but it’s harder to navigate.
Is http://www.cherrypy.org/wiki/TableOfContents better now? There’s obviously a bunch of re-org we could do with the individual pages, but at least there are full-phrase headings now.
Yes, actually, that is a vast improvement. Especially the headings under the Request Cycle. Thanks!
Robert, another issue I found:
http://tools.cherrypy.org/wiki/XmlRpcIntrospection
If you run this example, and use the xmlrpclib test it shows at the bottom, you actually get 404 errors from CherryPy. I created a ticket:
http://cherrypy.org/ticket/967
If I’m not mistaken you can have the same error handling feature from CherryPy by using the WSGI paste middleware for it
Here is an example:
http://tools.cherrypy.org/wiki/PasteEvalException
Very nice. I’ll give it a try. Thanks!