Okay, for starters, I don't think I've ever written anything so trollish before. Apologies in advance.
I had heard the criticism that Django had too much "magic" before I started working with Pylons. I thought to myself, "I have no idea how the ORM works," and I agreed. I thought to myself, "I have no idea how the request dispatch works," and I agreed.
So then I started working with Pylons. I learned two more things:
- I have no idea how ORMappers work, period.
- I have no idea how request dispatch works, period.
The problem wasn't that Django was magical; the problem was that I just didn't have the expertise and experience.
Anyway, after working heavily with Pylons for a few months, I got the opportunity to build a Django app for someone, and it was a breath of fresh air. I love the Pythonic way things work in Django, and the way I can express my model so well, the way I can put my program logic into views and model methods in pure Python code, without worrying about implementation details.
But all that is just icing on the cake. What I enjoyed most was how little magic there was.
Magic: import *
Explicit is better than implicit. By and large, Python is explicit. There are a couple of exceptions, and one of them are * imports. * imports should be avoided, and in public interfaces that other programmers have to use - like, say, a Pylons controller - are downright Considered Harmful.
Yet what is at the top of every Pylons controller? Take a guess.
- from projectname.lib.base import *
A little promiscuous with the global variables, no? Jeez, have some decency, cover up your namespace. How about...
- from django.http import HttpResponse, HttpResponseRedirect
Aaah. And then I can render a string with whatever templating engine I want, and write it to your response, because it's a file-like object. Thank goodness for the Pythonic duck typing!
- def index(request):
- response = HttpResponse()
- rendered_template = mako_render("filename.mako")
- response.write(rendered_template)
- return response
Heck, I could have guessed that.
Sadistic Magic
But hey, maybe I'm just being lazy. What I really want is to go look in lib/base.py to see exactly what's in __all__, right? Sounds good to me. "Surely", I think to myself, "that'll clear everything up for me; I can't judge Pylons too quickly."
- ## inside lib/base.py:
- __all__ = [__name for __name in locals().keys() if not __name.startswith('_') \
- or __name == '_']
My eyes! The Goggles, they do nothing!
To say the least, I don't understand the "Django magic" complaints anymore. * imports are magical. Dynamically generating __all___ is magic, not to mention sadistic and a crime against humanity. But mapping a url to match a regexp? Not magic. A model description language that doesn't require knowledge of SQL? Impressive, but not magic.
Sadistic, Useless Magic
What frustrates me the most about the incredible amounts of magic in Pylons is how little the programmer gains from it. The Pylons programmer can use the 'c' global variables to include context in a template - nevermind that this can be done much more intuitively by passing context as an argument to a render function, which also makes it trivial to dynamically generate said context. The Pylons programmer can use 'h' global object to do things that your models should really have as methods anyway. The Pylons programmer can use the 'g' global variable to - well, actually, no one knows.
The Pylons programmer can use the shortsightedly named1 'session' global variable to access the user's session. Gee, where else could session data be put, if not in a global variable? Well, hmm, maybe in an attribute of the request object. Nah, we'll use a global variable.
And if you actually want to access the request object? Well, instead of passing it to the function handling the request (intuitive) or maybe as an attribute of the controller object (OOPy), lets make it a global variable. Without a .session attribute, of course, because that's gonna be another global variable.
What Loose Coupling Is Not
Why not just have the session be an attribute of the request object? For one, it would couple the request to the session middleware! Oh, the horror - users wouldn't be able to transparently swap out Beaker for... that other session component people use with Pylons!
Pylons, as a framework, seems to be confused about what loose coupling is. Loose coupling between layers is not creating a new global variable for each layer.
What Loose Coupling Is
So what is loose coupling? Loose coupling is using native data structures, conventions, and idioms. Once you do that, programmers can predict how to swap out whatever they want.
Quick example: let's suppose we have controllers that are normal functions instead of methods of a specially-derived base class. Now let's suppose you want to do something like require authentication for a view. How would you easily apply some sort of authentication-wrapping to a function?
Easy. A normal, Python decorator.
And if you're using Pylons? Well, you have to look through the dismal documentation2 until, by chance, you find the Authkit Cookbook 3 which mentions the __befor\__ function. Funny, I don't remember seeing __before__ anywhere else in Python. And it's applied via some process not visible to the programmer... it's like magic!
Let's compare code for a second:
- @auth_required
- def index(request):
- # Actual controller body, auth_required applied as you'd expect.
Versus:4
- class BrittleNamingController(BaseController):
- def __before__(self):
- # auth_required code
- def index(self):
- # __before__ magically applied.
I hope I'm not the only one who sees how simplicity and loose coupling are one and the same. To change template rendering with Django, all you have to do is change:
- from django.template import Template
- def index(request)
- t = Template("String to {{ render }}")
- return HttpResponse(t.render())
to:
- from alternate_renderer import render
- def index(request):
- s = render("String to ${ render }")
- return HttpResponse(s)
There are no config files to edit, no eggs to install. You just do it, because you can predict how to do it.
Loose coupling is using native Python data structures
Loose coupling is giving us a file-like object as a request, not a global variable that is manipulated by a class with magical, arbitrary, undocumented methods.
Verdict
Django is by no means perfect, but they are so much closer than Pylons. You know, even if Django did have NIH, they "Invented" very useful components, so, Pylons devs, Steal from Django, PLEASE!
Okay, I think I got it out of my system. Except now I'm going to go work with Pylons for a few more hours. It's okay. At least I have my Django-powered, pinnacle-of-simplicity blog to post on.
-
Why badly named? Because SQLAlchemy uses session objects a lot as well. Perhaps, if Pylons were dimly aware of the fact that web applications use databases, it'd be more careful with it's naming. But no! We must loosely couple the layers, by giving each layer a global variable! Yeah, that sounds good. ↩
-
I've seen some people online actually saying that the Pylons documentation was good. No one contests that the Django documentation is better. Listen: if you think the Pylons documentation is "good, just not as good as Django's," you're given them orders of magnitude too much credit. The documentation is, in many cases, worse than useless, as we'll see with AuthKit in just a second. ↩
-
At this point in time, the Authkit Cookbook openly admits that it's out of date with the actual, public AuthKit module. And, of course, it's been out of date since this April, which is the last time it was touched. Brilliant! What do we do in the meantime? ↩
-
Am I the only one who's scared that Pylons needs your class to be named (controllername)Controller? What's going on the background to predict the controller name? You know what's magic is how Routes is able to refer the request to the right controller, by going from strings to class instances. That doesn't scare anyone? With Django URLs, you can actually pass objects, not just strings, to the url mappers, which means the name of it in the module doesn't matter - just give it a variable pointing to the right view. That's how actual Python works. A Brittle-y named class name, on the other hand, positively reeks of implementation details. ↩
Comments
100 spam comments omitted.
I am no longer accepting new comments.
Mike Lewis
#269, 2007-08-23T19:21:14Z
Interesting article. Do you think there's hope for Pylons? I'm thinking about doing a large scale (large as in users, not really features). Why did you choose pylons?
Thanks, Mike
Adam Gomaa
#268, 2007-08-25T11:17:09Z
I use Pylons only at work, because it's what our department has standardized on. The people making those decisions also weighed Django, but, having more experience than myself they decided that Django's relatively un-modular architecture would be a problem.
I use Django on this blog and am happy with it, although I can certainly see how there could be problems integrating with legacy applications - something I obviously don't have to do on a personal website, but is a staple of programming for my job.
Pylons has a future in terms of userbase, of course, just like RoR does. And like Java does, and like C++ does :). But that doesn't mean that you should use it. Depending on the complexity of what I'm building, my current projects go from Pure-Django, Django URL mapping and request handling but SQLAlchemy models and Mako for templating, to no Django whatsoever - a pure WSGI stack.
But I don't use Pylons now where I can avoid it, and until they fix their configuration and the amount of magic I probably will keep avoiding it. Pylons gets a few things done better than Django - such as using Mako and SQLalchemy - but they also get a lot done worse.
And with Django, I can fix the problems, because I can understand how they do it. With Pylons, the problem is that the public interface they provide is brittle, verbose and magical, so I couldn't guess how to swap out Mako for another templating language, or how to swap out SQLAlchemy. With Django, I've done both of these, and I was able to do it without editing config files, installing eggs, or begging for help on IRC :). That's the primary difference that I see between them right now.
Philip Jenvey
#286, 2007-09-04T23:07:38Z
The from project.lib.base import * in Pylons controllers is very much intentional. Yes, most of the time import * shouldn't be used, but there are cases like this one when it is useful.
The base module is essentially your project's controller API, a namespace you really want shared in all Controllers. The mailing list post you reference says: ""from X import *" is almost universally shunned because it fills your namespace in ways that you can't really control". You're not importing * from the sys module like in the mailing list post; lib.base is always under your control. A big benefit of import * here is DRY.
base's __all__ is a list comprehension because unlike most __all__s, it augments the public names (adds '_') instead of taking away from them. I'll note that I've seen a similar list comprehension in Python's sys and tokenize module. Though it's a valid criticism; I think we're going to make __all__ explicit in 0.9.6 just so it's that much clearer. You're also correct in that __before/after__ need to be documented more.
The point of Buffet is the ability to switch templating languages and maintain the same rendering API as much as possible. In your templating engine swap example, you switch the imports, but you also have to change the code (just call render() now instead of constructing a Template then rendering); you have to accustom yourself (and maybe your existing code) to a new API.
You mention a file-like request (I think you meant file-like response) object in Django a couple times. I'm not sure you noticed, but Pylons stole that from Django well over a year ago: response.write('hello world') =]
Ben Bangert
#287, 2007-09-04T23:31:09Z
* imports are "generally" avoided, because 'generally' you are importing from a module not immediately present (some system library, etc.)
You like DRY, right? If you need to make a auth function available in all your Django modules for use, how many times are you going to write that import statement? O(N) where N is however many Django views you have. Not very DRY...
The purpose of that import * is so that you can easily control the names imported in all your controllers, in a sane (because the module its importing from is in your project) DRY manner. If the * import was a sin against mankind, as you seem to think it is, Guido wouldn't have put it in the Python language. Look through all your Django views and see the incredibly amount of repeating import statements.
Take a look in Python's standard library modules 'tokenize.py' and 'os.py' where you'll see similar "Sadistic Magic" with regards to the __all__ import line. This is needed in many cases to avoid having do declare massive lists of names for export, and is done in Python's standard library itself.
Why is that there? Because Python doesn't import any name beginning with '_', and when using i18n, the convention is to use the _() function. How can we make sure to export all the names in the base.py module including the '_' function, but not the other names that start with '_'? The 'Sadistic' line does that of course.
If you're not using i18n, feel free to remove the __all__ line, and explicitly declare the names you want exported. It's your project, under your control, there is no Pylons magic occuring, just normal Python imports in your project where you are free to change it as desired.
With regards to the session, the request object is the "request", the session is not part of the request. Why would the session be on the HTTP request, when it clearly isn't? (The session is on your server, only a cookie is in the request)
On the 'c' object, that's partly to make it easier to pass variables to the template, partly to keep them underneath a known namespace in the template rather than dumping them into the template variables as globals.
The 'h' is a namespace for your helpers module, which is intended to be used for template helpers, not model helpers. Putting your template helpers on the model objects is a pretty odd desire, why do you want to do that?
Why have a global for the request? This might shock you, but almost every web framework, in every programming language I've used does this. Let's take a look at why, imagine the rather common case that your view (controller) function calls another function to determine something, it then calls another function, maybe 2 or 3 functions, (big webapps do this). Now some function 4 function calls away from your view function needs the request object to see something... ouch, now you're left passing a request object 4 function calls down even though none of the intermediary functions need the request. That hurts.
I'm open to feedback on how to make Pylons better, but I really do think your complaints are failing to take into account major problems and utter lack of DRY they actually cause.
Adam Gomaa
#300, 2007-09-05T15:43:37Z
I'm actually surprised that it's common for web frameworks to use a global for the request. (My punishment for having used Django first...) Passing it as an argument seems so much more intuitive.
But what's the problem with passing the request object down the request chain? Not for performance; Python will pass it by reference. And it shouldn't be to save typing, because you actually want the request going down the request chain.
Ben, the auth function example is interesting, because you're totally right in theory. But the thing is, that never happens. I don't have ridiculous numbers of imports sitting at the top of every file, and the benefit of being able to isearch my code and see where every function and object comes from is enormous.
The token, symbol, etc etc modules are perfect examples of where __all__ generation is needed, because their purpose in life is to export constants. My Pylons project isn't; I'm using application objects and functions, and for application code, I like to import the things I need by name.
Phillip: yeah, I meant response... and you only halfway borrowed the file-like response: it's still a global variable.
But I'm glad to hear that lib.base.__all__ is going to get explicit. I found that while chasing down a heisenbug and didn't take kindly to it ;)