Django web framework: A (brief and incomplete) review

Here are my thoughts after working with Django web framework for three weeks. Because Laravel + PHP (which is heavily inspired by Ruby on Rails) is my usual go to stack, this is what I’ll be comparing it with. Your mileage may vary, of course. This review assumes ‘all else being equal’ - if I feel that Django does something as well as Laravel, I won’t mention it.

The Good

Django ORM: Magic that actually works

Django ORM is incredible. Automatic migration generation from models sounds like magic, but it works so well in practice that I never want to go back to writing migrations by hand. Just two days ago I had redo most of my models because I realized that the polymorphic query was not working out. I took a knife to my models and split four models into six with each of the three pairs of models inheriting from an abstract model. This took almost no time at all thanks to the fact that I only have to care about what the model would look like, and not the logistics of transforming the previous schema to the new one.

Granted, the autogenerated migrations don’t work perfectly in all situations. Any sort of complex data migration that involve mapping existing data is best done by hand. But in most cases Django’s ORM works well enough, and it catches a lot of errors I would have made writing migrations by hand.

In addition, QuerySets work incredibly well. While the syntax may seem slightly goofy at first (for instance, the double underscore in parameters such as filter(pk__in=user_ids), the flexibility of Python’s OO design really shines here, as it allows extremely expressive queries to be expressed with the minimum amount of code, and query objects to be composed using the same operators you would use with sets, because that’s really is the best way to think about relational databases.

Python

Python is awesome. It is clean, expressive, beautiful, well designed, and the nicest language I’ve used. ’Nuff said.

The Bad

Routing: Using a sledgehammer to crack a nut

Django uses regex to specify its URL routing patterns. This is an example straight from the Django documentation.

r'^articles/(?P<year>[0-9]{4})/$'
r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$'
r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/(?P<day>[0-9]{2})/$'

In comparison, here’s how you’d do the same thing in Laravel

'articles/{year}'
'articles/{year}/{month}'
'articles/{year}/{month}/{day}'

Another example - Stack Overflow questions would have URLs looking like this with Django r'^questions/(?P<question_id>[0-9]+)/(?P<slug>[^/]+)?/?$'. The same in Laravel - 'questions/{question}/{slug?}'.

Using the full power of regex to specify URL is completely insane.

Forms: abstraction gone mad

Here’s a simple premise: HTML is not hard to write. Keep that in mind as we go through this section.

Rendering a form in Django potentially require touching up to six distinct classes.

  • FormSet - a group of Forms, usually used for forms of objects with one-to-many or many-to-many relationships
  • Form - a single form with many Fields
  • Model - the data layer that the form binds to, the class representing a database table and each instance a row in the table
  • Field - a single distinct piece of data in the Model, a column in a database table
  • Widget - in charge of rendering the HTML and validating the value returned from the request
  • Renderer - used to by some Widgets to control the resultant HTML

If the form you’re looking for is exactly the one Django gives you, congratulations! You’ve saved yourself the hassle of writing some HTML. If it doesn’t, which is almost certainly the case, you’re in for a world of pain.

First, you need to decide if you need Django’s form-model binding. If you do, you’ll need to use a ModelForm instead of a Form, which has an entire page of documentation all on its own and involve some metaclass magic. If you’re editing a one-to-many relationship (students in a class, answers on a question, lessons under a module), you’ll need a FormSet or a ModelFormSet, but to create that you’ll need to use the formset_factory method.

Next, look for the class, parameter, property or method you may need to override. Adding attributes to a form element? Probably add an attr parameter. Need to update the HTML structure of the form element? Might need to swap out the field’s Widget with either another one that comes with Django, or build your own. Need to combine some fields together, a task I was trying to accomplish before we pivoted so that users can edit and select the correct answer to a question at the same time? In HTML it would be seven lines of markup. In Django? Goddamn impossible.

At this point you’ve probably written or overriden a few classes, made a call or two to the modelformset_factory to spit out a few more classes. You look back at this mess of AnswerSetForms and HTML inlined into Python with format_html and realize that you’re getting bad flashbacks to Java. There’s absolutely no reason why the code to generate the form should have more lines of code than the actual form in HTML, right?

So you throw away all that and write your form in template instead. Now you need to validate the resultant input. Unfortunately Django has no distinct validation service. Instead, validation is bolted onto individual Form, Field, Widget and Models objects, and while you can validate the model’s data directly, this isn’t what it’s designed to do. For instance, relations cannot be checked in a model’s clean function, but must be handled at the Form level instead.

tl;dr: Django’s form classes is an example of too much abstraction. Using objects to represent forms - especially HTML forms - is a bad idea, because they are semantically very different things, and in the end you’re not actually saving much work unless you have a clinical condition that prevents you from writing HTML.

The Ugly

Settings and setup

Django’s setting system leaves much to be desired. All settings are stored in a single settings.py file. This has two implications.

  1. Because sensitive settings are not read off another file that is not checked into source control (like the .env convention used by Rails or Laravel), it is exceedingly easy to accidentally check sensitive settings into the repo and potentially exposing it to the public.

  2. All settings share the same namespace, so some settings require very long names to avoid collision. Laravel avoids this by exploding the config into multiple files. This has the added benefit of allowing vendor packages to add their own config by simply copying their default config into the project’s config folder and letting the user tweak that.

In addition, some of the defaults that Django comes with are frankly baffling. When the framework encounters an error in production (ie. with DEBUG set to off), Django by default will send the error report to the admin via email. Unfortunately the settings do not ask you to configure the mail server settings so that it can actually send out the emails. In addition, the error reports can contain sensitive information, and sending them via email is in fact discouraged by Django’s own documentation.

Closing thoughts

Django sells itself as The web framework for perfectionists with deadlines. They’re not wrong - many of the features are amazing if you need to move fast. The automatic migrations, the very high level of abstraction, the declarative style of building forms, models, admin panels, the automatically generated CMS panels and many other parts of the framework seem extremely well suited for rapid prototyping.

Unfortunately all these come at a cost. The abstraction can frequently leak, and it can be hard to uncouple the parts if you try to go lower level. The framework is great if what you want to build can be assembled using the pieces found in the framework. It can be a nightmare if it isn’t.