Django as it is in the wild is not a web software framework. It is a web site framework.

(disclaimer re. complaining: http://commandcenter.blogspot.com/2011/12/esmereldas-imagination.html )

What does this mean and why?

For some things, I love Django. It hits a particular sweet spot really well. That sweet spot is deeply informed by the circumstances that birthed Django.

Typical example: News organization wants to put an interactive feature online in one day. The programmer has a days' work to do on building the public-facing site. Reporters need to do data entry and curation.

With Django, the programmer can build a dashboard (a UI built with django.contrib.admin) in literally a few minutes, and the reporter can then start working in parallel while the programmer gets the rest of her job done. This is pretty awesome. (It also reminds me a lot of the old days of Zope 2: The ZMI (Zope Management Interface) was sometimes leveraged in a similar way as django.contrib.admin UI, although the ZMI's design, being much older, now seems quite dated and inflexible by comparison.)

I have not seen anything as good - for this kind of thing - as django.contrib.admin. It remains the killer Django application.

Django falls short when trying to build reusable systems, and when trying to integrate third-party code into a site or application.

This is because "Apps" as typically written are not reusable:

So, an app as typically written is not a library. It's a self-contained website. It may claim to be reusable, but typically you can use it whole-hog, or not at all; and somewhere in between, the integrator's job gets really difficult.

Why is that? A typical app consists largely of:

  • Templates

  • Model classes

  • View functions

Let's look at each in turn.

Templates

This is where Django offers the easiest reuse.

Typically you just need to create a template with the right path and make sure that path is found early, which may mean adjusting the order of settings.INSTALLED_APPS or settings.TEMPLATE_DIRS. No problem.

Overriding part of a template - a single block - without changing the template's name (so you can use it in views that use that template by name) is also easy - but not obvious if you don't know how. The trick is to save your copy with the same name and same relative path, but instead of starting it with {% extends foo.html %} you start it with something like {% extends "appname/templates/blah/foo.html" %} and make sure that appname/templates is in your settings.TEMPLATE_DIRS. Got this from https://code.djangoproject.com/wiki/ExtendingTemplates

So, we're in pretty good shape with respect to Templates. Yay!

Models

Django implements two flavors of model inheritance: one with an explicitly abstract base class, and "multi-table inheritance" where the subclass instance is a sort of proxy to an instance of the superclass, and stores its extra data in a separate table.

This is great when you want to have multiple flavors of the base class. But sometimes, when leveraging third-party models, you just want to add a little bit of data, and effectively substitute your model for somebody else's (see Views below for why this is crucial).

Multi-table inheritance is okay as far as it goes. Incurring the overhead for an extra table might or might not be a performance problem in a given case. But there's no way to avoid it that I know of ... except to copy/paste the model code. Or monkeypatch, which should always be a last resort in general, and I don't know the semantics of monkeypatching models (it looks like to add a field you need to call add_to_class and/or contribute_to_class and then write a migration to add your field)... so the idea scares the crap out of me.

Views

It's actually quite easy to replace a view, if that's what you want - it takes one line in a urls.py file.

The problem is fine-grained reuse - i.e. taking an existing view and tweaking it slightly. View functions do two things that inhibit fine-grained reuse:

1) Almost always refer to a specific Model class. This means if you want to use a subclass or alternate implementation of the model, you need to copy/paste all the view code, or use monkeypatching to swap out the model imported by the views.py module - which may be too blunt a hammer depending on what you're trying to do.

2) Views return an HTTPResponse object, not data. I would like later binding of data to response. Views typically explicitly create the HTTPResponse from a particular context dictionary and a particular template name, and after that you can't do anything interesting with it. (The template is looked up by name at runtime - late binding there, yay.) This means you can't easily override or enhance the context used by the template to render that data.

In the worst case, you have 15 third-party hundred-line view functions and all you want is to change one little thing somewhere in each one, like swap in a different model implementation, or add or modify a couple objects in the context dictionary... and you end up copy-pasting all 15 views. Ouch.

This problem actually already has a solution, in the form of class-based views, as of Django 1.3; it's just that there's so much historical code using the old view function patterns that, often, we can't use the solution when overriding third-party views.

I can't really fault the Django developers for failing to anticipate these problems way back in 200-whatever; maybe Guido's time machine wasn't available; but I selfishly wish class-based views had landed earlier with better narrative docs.

Class-based views are also indisputably more complex, which is likely to slow their adoption, because it takes effort and thought to learn how to use them properly, whereas anybody who's ever read a five-minute django tutorial can whip up function-based views all day. By comparison, here's the class-based view docs: https://docs.djangoproject.com/en/1.3/ref/class-based-views/ (and this one is a much easier read: http://www.caktusgroup.com/blog/2011/12/29/class-based-views-django-13/)

The reason for the complexity, of course, is useful features. For problem #1, they give you a lot of plug points, either in a view subclass or in urls.py, to swap out model implementation: the model, the queryset, get_queryset(), get_object(), ... All great. But it's more to understand.

For problem #2, class-based generic views provide a hook to augment or override the context: https://docs.djangoproject.com/en/dev//topics/class-based-views/#adding-extra-context

An alternate solution to #2 would be if rendering wasn't directly the job of the view function. A framework that works that way is Pyramid. An example may make this clearer.

Here's one typical way I could write a view in Django:

# myapp/views.py

def my_view(request):
    # ... 100 lines of logic and gathering data.
    # Then put all that data in a dict...
    context = { ... }
    # ... and render it to an HTTPResponse object.
    return render_to_response('template.html', context)

So let's say I've released 'myapp' as open source, and you find it and want to re-use it in your project, and you want to make a view that returns the same data as my view, with the data slightly altered, or just a little bit more data mixed in... like this::

# yourapp/views.py

from myapp.views import my_view

def your_view(request):
    context = my_view(request)   # Errr, that's already an HTTPResponse.
    context['foo'] = 'bar'   # So this is impossible.
    return render_to_response('template.html', context)

You're sunk. You can't write your trivial wrapper without copy/pasting my entire 100-line view function into your code.

(Interestingly, the Django testing infrastructure provides a way to get the context object out of the HTTPResponse... but that's not normally available outside of the testing context, and doesn't avoid the overhead of rendering the template anyway.)

A workaround I've occasionally resorted to is writing a context processor that inserts the data I need, returned as a callable (so it's lazily evaluated). This only works if the existing view uses a RequestContext instance, which you can't control from the outside. The idea is that, because of the context processor, all views have access to the data it returns; by making my value a callable, it's lazily evaluated only when needed. Not tidy, but it gets the job done.) Now, if I'd had the foresight to split my_view view into two functions - one returning data, and a trivial wrapper that renders it to a HTTPResponse - then you could do what you need:

 # myapp/views.py

 def _my_view(request):
     # ... 100 lines of logic and gathering data...
     context = { ... }   # put all that data in a dict...
     return context

 def my_view(request):
 # ... and render it to an HTTPResponse object.
     context = _my_view(request)
     return render_to_response('template.html', context)

 # yourapp/views.py
 from myapp.views import _my_view
 def your_view(request):
     context = _my_view(request)
     context['foo'] = 'bar'
     return render_to_response('template.html', context)

But that's not the documented pattern, so thus far it's rarely done, so you can't.

The class-based generic view solution would look like this::

 # myapp/views.py

class MyView(DetailView):
    def get_context_data(self, **kwargs):
        # ... 100 lines of logic and gathering data...
        context = { ... }   # put all that data in a dict...
        return context

# yourapp/views.py

from myapp.views import MyView
class YourView(MyView):
    def get_context_data(self, **kwargs):
        context = super(MyView, self).get_context_data(**kwargs)
        context = ['foo'] = 'bar'
        return context

Yay! That works.

(I fear that even with the existence of class-based views, people will probably continue to follow the path of least resistance and write one function per view. Or maybe I'm just speaking for myself since I haven't put a single class-based view in openblock yet. Time will tell.)

I said there was another possible approach. In Pyramid, the documentation shows you how to have your view function just return data, eg. a dict; and a bit of view configuration outside the view (typically a one-line decorator, but there are other ways to do config) hooks that data up to a template (or other rendering mechanism) which is used later to render that data to a response object.

In pyramid, then, if you call my view in your code, the view config on my view doesn't matter; if you call the view from your function then you just get my returned data:

# myapp/views.py
@view_config(renderer='template.mako')
def my_view(request):
    # ... 100 lines of logic and gathering data...
    context = { ... }   # put all that data in a dict...
    return context

# yourapp/views.py
from myapp.views import my_view

@view_config(renderer='template.mako')
def your_view(request):
    data = my_view(request)
    data['foo'] = 'yay!!!'
    return data

@view_config(renderer='template.mako')
def your_other_view(request, context):
    data = your_view(request, context)
    data['bar'] = "i can wrap my own views too, of course"
    return data

Simple. Beautiful. I wish Django worked like that.

Not enough plug-points.

Example: no signals on update().

My experience with this went something like:

  • yay, Django has signals!

  • "I want X to happen any time an instance of model Y is created/updated/deleted"

  • delete() and save() send signals, I can hook into those, great!

  • update() does not, by design, send any signals. It's supposed to be a "close to the database" optimization -- there's fear that somebody might write an inefficient handler and thus make update() slow. Closed wontfix: https://code.djangoproject.com/ticket/13021

    I just can't understand this way of thinking. We're all consenting adults here (http://mail.python.org/pipermail/tutor/2003-October/025932.html)

  • So: I can't use update(). (Side note: So I guess I'll call .save() 100 times in a loop. 100 hits to the database plus 200 signals, instead of 1 hit and 2 signals. Yup that sure does sound like an efficiency win.)

  • "Why don't you just send your own signal." Sure, if everything is my own code.

  • third-party code means you can never be sure that nobody else is calling update(), and there's no way to make third-party code send my custom signal.

  • I guess I'll monkeypatch, or write database triggers.

  • Seriously?

Misc.

Not sure if this belongs in a separate post, but, there are various kinds of weirdnesses where things aren't hooked up. One example:

Form instances do not have access to the request, at least not without a hack like http://stackoverflow.com/questions/1057252/django-how-do-i-access-the-request-object-or-any-other-variable-in-a-forms-cle

Use case: I'm rigging up a Model class to the admin UI and want to invoke the django.contrib.messages framework if some field is saved with some value. I can't just implement Form.clean() and be done, instead I have to rig up a custom view. (If I'm already writing a view, this is fine, but django.contrib.admin is important.)

Widgets are another thing that don't normally have access to the request, which strikes me as odd.

Postscript re. Rails.

I'm told that reuse in Rails doesn't offer nearly what Django does, even with all my gripes. Can that be true? Amazing.

Apparently the feeling is that, because reuse is hard, it's bad: http://whynotwiki.com/Rails_/_Philosophy/opinion


Comments

comments powered by Disqus