A good rule of thumb in all things on the Internet is to take whatever's said and tone it down in your mind by an order of magnitude to get an accurate picture.

I really enjoy working with Django. It gets a lot of things right; I think it does better than the competition in most respects. Nevertheless, it's true that if you think a piece of software is "perfect," you can't be an advocate; you're just wrong. So while I like working with Django, I have, of course, run into things that I would prefer implemented some other way. I am sure there are reasons that things are the way they are; I'm not going to propose many solutions (if I were, I'd be writing patches, not articles). All I can offer is feedback.

I should note that it took me a long time to come up with five problems. That's a testament to how well most of Django is put together. In line with previous iterations of the "five things I hate about" meme, though, I waited until I'd reached five before posting this.

Software, by its very nature, can never be perfect. If we choose to seize upon, expand and write about these imperfections, we might just come with ... Five Things I Hate About Django.

5. Models, aka, Where's the SQLAlchemy?

The django-sqlalchemy branch was first announced almost a year ago. Since them, for all practical purposes, nothing has happened. The good news is, they're looking for people to do it.

So hey, my prerogative to get involved, right? Right... while we can certainly blame the people (myself included1) who want to use SQLAlchemy in their web frameworks, you have to admit that we use frameworks in the hopes of spending less time dealing with implementation details and more time with high-level, fast, and effective problem-solving.

It's not all bad. Django models have a great interface2 . Recently I've been playing with using [SQLAlchemy models with Django], and while the technical side is working just fine, there is no doubt that I'm thinking in SQL more than in Python objects. SQLAlchemy table description code is more verbose, and much uglier, than Django models3.

But here's the problem for Django: SQLAlchemy is progressing, and pretty fast. Django's ORM just isn't moving at the same speed. While Django's models are nicer in interface right now, they are falling behind with regards to multiple databases, as well as supporting some engines that SQLAlchemy already has working (and debugged). Furthermore, SQLAlchemy's upcoming 0.4 release will help with a lot of the verbosity. The rather confusing steps that ORM requires in SQLAlchemy - going from DB engines to Metadata to mappers and finally to objects - is already being simplified, both in the SQLAlchemy development and with third-party tools such as SAContext. Simply put, SQLAlchemy's interface simplicity is closing with Django's ORM far faster than Django is closing the technical advantages4 that SQLAlchemy has.

4. Non-Inheriting Models

As I've noted before, one of the things I like about Django is that it has a very Pythonic feel, and gets many things right in a Pythonic way. Model definitions are, like I said, pretty neat. Unfortunately, they depart from regular Python classes in that they are not subclassable. Again, contrast this with SQLAlchemy, in which the objects are pure-Python and even the highly difficult problem of database-level inheritance is addressed - very well.

Inheriting model descriptions no doubt has technical challenges. I personally find the metaclassing involved with regular ORM mapping to be at the limits of my abilities, and I can only imagine what dealing with inheritance in metaclasses would involve. Unfortunately, your typical Django "user" - a Python developer - is used to Python's object-oriented nature. I think that it would be worth it to make such a fundamental part of OOP as class inheritance available for use by the programmers using Django.

3. Package URLs & Newforms, Please!

Django has a (unfairly earned?) reputation for NIH syndrome. Merits (or lack thereof) of the charges aside, I find that I wish other frameworks would copy Django's URLs; they alone are nearly enough reason to choose Django over most any other framework. Certainly there are other factors, but the the URLs outshine Routes and CherryPy so dramatically that working with either of them makes me quickly miss Django's URLs.

What I'd really like to do is to take the URL mapping module (django.core.urlresolvers seems to be the place to start, if anyone's interested) for use in any framework I need to work in. If nothing else, it would cure the NIH grumbling!

And while we're at it, can the same be done for newforms? I've heard a lot about it, and forms are such a hard thing to get right that getting anything but across-the-board negative reviews is pretty significant. Unfortunately, I'm unsure enough about sticking with Django for some of my projects that I'm holding off on writing my forms with newforms, in the event that I end up using something else. Now, if I knew I could take newforms and, with some effort, get it working with other frameworks, and if it's really as good as I've heard, then I'd use it, no doubt. But I don't want to be bound to Django by something that shouldn't bind me to Django, like a forms module. It's worth it to make modules portable5 to multiple frameworks.

2. End Middleware Confusion (aka, Mod_python is a Lame Excuse)

Django's preferred deployment is mod_python. Unfortunately, for most people, a WSGI deployment makes a lot more sense; in a shared hosting environment (like this one) a hosting provider is unlikely to have mod_python, but pretty likely to have something like FastCGI, for which WSGI wrappers exist en masse. For high-volume installations, mod_python might be a better deployment - but that's not an argument against WSGI. mod_python can run WSGI apps, and if Django needs a mod_python deployment, it should do so as a WSGI app.

The reason for this is that Django middleware is... troubling. Middlewares like Gzip encoding, user-agent filtering, and ETag caching don't belong in an application framework. They belong in the server. Is it convenient? Usually. It it correct? No.

Now, if the Django devs don't actually like WSGI middleware, they should do it properly and be opinionated. But a technical excuse that isn't genuine is worse than an "agree to disagree" situation that CherryPy and Paste seem to have reached.

Oh, and mod_wsgi is coming along fast - the 1.0 RC is out as I write this. Excuses for mod_python are running out...

1. Settings are So Close to Being Perfect

One of my biggest continuing issues with Pylons is the configuration. The ini files are cryptic, magical, and full of settings that are documented in varying places, if at all. Furthermore, you can't easily see6 where it's being used. They're such a hassle that my only goal with them is to not break my application, and even then, I'm only usually successful.

In contrast, settings in Django are fantastically well done. The settings.py file is easy to understand, extremely well documented, useful, usable, and clear. Seeing where it comes into play is easy too; the django.conf.settings object provides easy access to it. Instead of an arbitrarily set of attributes in an ini file, it's a runnable Python module. So if I'm gushing about it so much, what's the thing I hate about it? This:

  1. adam@at:~$ python
  2. Python 2.5.1 (r251:54863, May 2 2007, 16:56:35)
  3. [GCC 4.1.2 (Ubuntu 4.1.2-0ubuntu4)] on linux2
  4. Type "help", "copyright", "credits" or "license" for more information.
  5. >>> help('modules')
  6. Please wait a moment while I gather a list of all available modules...
  7. Traceback (most recent call last):
  8. File "<stdin>", line 1, in <module>
  9. File "/usr/lib/python2.5/site.py", line 351, in __call__
  10. return pydoc.help(*args, **kwds)
  11. File "/usr/lib/python2.5/pydoc.py", line 1646, in __call__
  12. self.help(request)
  13. File "/usr/lib/python2.5/pydoc.py", line 1683, in help
  14. elif request == 'modules': self.listmodules()
  15. File "/usr/lib/python2.5/pydoc.py", line 1804, in listmodules
  16. ModuleScanner().run(callback)
  17. File "/usr/lib/python2.5/pydoc.py", line 1855, in run
  18. for importer, modname, ispkg in pkgutil.walk_packages():
  19. File "/usr/lib/python2.5/pkgutil.py", line 125, in walk_packages
  20. for item in walk_packages(path, name+'.', onerror):
  21. File "/usr/lib/python2.5/pkgutil.py", line 125, in walk_packages
  22. for item in walk_packages(path, name+'.', onerror):
  23. File "/usr/lib/python2.5/pkgutil.py", line 110, in walk_packages
  24. __import__(name)
  25. File "/home/adam/.pylibs/django/contrib/databrowse/__init__.py", line 1, in <module>
  26. from django.contrib.databrowse.sites import DatabrowsePlugin, ModelDatabrowse, DatabrowseSite, site
  27. File "/home/adam/.pylibs/django/contrib/databrowse/sites.py", line 2, in <module>
  28. from django.db import models
  29. File "/home/adam/.pylibs/django/db/__init__.py", line 7, in <module>
  30. if not settings.DATABASE_ENGINE:
  31. File "/home/adam/.pylibs/django/conf/__init__.py", line 28, in __getattr__
  32. self._import_settings()
  33. File "/home/adam/.pylibs/django/conf/__init__.py", line 53, in _import_settings
  34. raise EnvironmentError, "Environment variable %s is undefined." % ENVIRONMENT_VARIABLE
  35. EnvironmentError: Environment variable DJANGO_SETTINGS_MODULE is undefined.

So close... so close...

If you actually look in db/__init__.py, which is part of the chain that ends up hitting the error, you find that if it doesn't find a database, it uses a dummy one instead. This kind of fallback is what django.conf.settings should do too: return a None for all accesses if there's no DJANGO_SETTINGS_MODULE.

  1. Yes, I have considered giving this branch a shot, but I just don't think I have the experience - Django's models and SQLAlchemy are really the only two ORM systems I've used. I don't think I even knew what ORM stood for eight months ago. I'd be happy to help with patches and particularly documentation if someone with the right experience is the lead maintainer, but I'd be doing the branch an injustice to claim I had the level of expertise needed. 

  2. Relative to SQLAlchemy, it's not that far from Storm. Convergent evolution, perhaps? 

  3. Some parts of Django models can get ugly if you're trying to do something a bit weird. The quick example I can think of is the need to override save() if you need to update fields before coming to DB, which is very common when you need to escape HTML, etc. On the whole, though, Django models look more like Python models than SQL statements; the same isn't true of SQLAlchemy (declarative layers such as Elixir notwithstanding). 

  4. Besides the already-mentioned advantages of more engines and multi-db support, SQLAlchemy also has the low-level accessed needed to implement effective migrations. One Django migrations tool, for example, copies the entire database

  5. Keeping my models framework-agnostic with SQLAlchemy has already paid off. I was able to cp -r my models/ from a django project to a CherryPy project when I was trying out CherryPy, and everything worked perfectly. I found out within a day that CherryPy wasn't what I was looking for (although a port of Django URLs to CherryPy would be interesting...) but I couldn't have done it so painlessly if not for having used SQLAlchemy in the models. 

  6. While we're at it, what is it with Pylons' obsession with global objects and * imports? * imports should be used only if you know what you're doing - and not at all in public interfaces. That they're a staple of Pylons development is extremely frustrating.