Why class-based views are not all that great

Disclaimer: the views and opinions presented here are mine and do not reflect those of my employer, Mirumee Software.

Class-based views were first introduced in 2010 and back then my younger self did his best to push back on the current approach. Unfortunately I was so upset with the initial implementation that in the end I came across as a condescending asshole, apologies for that. Six years later I still try to avoid them and I figured I might as well share some of the reasons with you.

Before I continue I want you to know that I do recognize that class-based views are a tool and as such may be a valuable resource. There surely are cases where class-based views provide an adequate solution to a problem. The part I find troubling is that it seems to me that they are being widely accepted as the only way forward.

Working with class-based views is a journey in a maze full of dead-ends

Working with generic views and class-based views in particular is pretty straight-forward when your goal is a well-defined stationary target. That’s why these views work so well in tutorials.

Unfortunately programming is nothing like that. Your customers (even the internal ones) have their business requirements and the goal posts move with every iteration. When those requirements change class-based views often force you to take a dozen steps back before you’re able to move forward.

Let me stress this: moving from one base class to another is often a prohibitively expensive refactoring task.

At least in the end you write less code, you saw that in the examples. Well, the “generic” part turns out to be a marketing lie. The promise of having a generic solution to every problem is tempting but each generic view is a tiny rabbit hole.

Adding a form to a detail view? Just a couple of lines. Adding a second form to a form view? Easy enough. A third form? Done. The code is a mess but you keep digging deeper. There’s rarely an obvious place to add your changes so most of your business logic lands in get_context_data that eventually starts to resemble a traditional function-based view. Except its surrounded by all kinds of one-line method overrides for things like get_success_url that have long since lost their original meaning.

Of course you can keep refactoring logic into new base classes but under what circumstances does a generic “three forms where the third one is only validated if the second contains a Thursday” view start to sound like a good idea? Once you rename form_valid to wishlist_form_valid all of the time-saving form mix-ins become useless.

And if requirements change do you repurpose your old view or do you come up with a new semantic abstraction and a bunch of new mix-ins?

Here’s a proof that class-based views are hard to get right: Django itself has recently accepted a pull request that resulted in a security hole caused by conveniently stuffing view logic into get_context_data.

Debugging is very hard

Most of the class-based view logic is implemented by generic methods. Methods themselves often live in mix-ins. And you’re encouraged to only override some attributes and helpers and let the generic code implement control flow and abstract the complexity away.

Here’s the catch: a typical stack trace will contain very little if any of your project’s actual code.

Most generic views will also helpfully implement a variant of a get_template_names method which saves you from the cost of having to provide explicit template names in your code which seems like the most convenient idea ever until you stumble upon a seemingly unused template file and have no idea how to check whether it’s actually required.

How does one reason about the code that they don’t see? And more importantly how does it work for someone with little experience? I’m not even referring to general programming experience as class-based views are a tiny domain of knowledge specific to Django. Working with these views is simply not possible without a cheat sheet or an IDE like PyCharm.

The implementation is a mess

Since the logic is split between many methods that often come from unrelated mix-in classes, there’s no way to maintain a common scope without modifying the instance itself. To allow this Django forces you to call the as_view class method.

It creates a local function bound to the list of arguments passed to as_view. Once called the function instantiates the class passing the previously bound arguments. It then assigns actual arguments passed by the router and request to instance attributes. It finally calls dispatch passing in the very same request and arguments as parameters.

Why does it pass them both as arguments and as attributes? There’s probably a reason but it’s far from being the only thing that raises questions.

For example trying to write a custom class initializer quickly leads to a “wat” moment:

It turns out that as_view only accepts arguments that are valid names of existing class attributes. Except for attributes that correspond to valid HTTP method verbs. The default class initializer will just take whatever you throw its way and assign it as an instance attribute effectively allowing you to shadow most of the attributes and some of the methods.

Which means you can’t do this:

But it’s perfectly valid to do this:

Or even this:

You can even pass in a bunch of useless stuff as long as the names align with one of the base class attributes:

The generic views provided by Django depend on multiple-base class inheritance which means you have little to no control over what super() refers to in your code. There’s no obvious way to skip a particular level or force one mix-in to run before another save for introducing an MRO-mangling meta-class.

And if you need to use a decorator you either have to decorate the function returned by as_view (which usually means putting the decorator in your urls.py) or live with a super-awkward dispatch override. Below code is a trivial example that could be replaced by an existing mix-in but not all decorators are available in mix-in form:

Where do I go from here?

Now, I’m not trying to tell you to go and rewrite your code. There are use cases where class-based views really shine. They are very good at scaffolding, especially if you can avoid subclassing them entirely:

book_details = DetailView.as_view(model=Book,
template_name='book_details.html')

What I recommend developers avoid is trying to repurpose a generic view to do more than it was designed to do. Passing an extra context variable is probably fine but even at this point you’re likely ending up with more code than would be needed to implement the same logic as a function.

The class-based views are not all bad but the scope of their usefulness is limited. Here’s to function-based views, I hope they outlive us all.

Show your support

Clapping shows how much you appreciated Patryk Zawadzki’s story.