Django’s class-based views are powerful and customizable, but unless you have a solid mental model of how they work, they’re also confusing and tricky to master. It’s easy to use class-based views without a deep understanding of their inner workings, which leads to Googling for which method or attribute to modify rather than confident usage of your tools.

In this post, we’ll walk through the base classes Django uses its class-based views. You’ll gain a useful picture of how class-based views work under the hood and how they differ from function-based views.

Hopefully, this will make you more comfortable working with class-based views and less dependent on StackOverflow in the future.

The entirety of this post focuses on a single Django file, /django/django/views/generic/base.py, which contains the classes that live at the top of the view classes’ inheritance trees.

If you’re looking for something specific, you can jump to that section below.

View

Right away, we can see that there is a base view class. RedirectView builds on the functionality from View . TemplateView also builds on View , and includes functionality from the ContextMixin and the TemplateResponseMixin .

Here’s the code from View .

class View: """ Intentionally simple parent class for all views. Only implements dispatch-by-method and simple sanity checking. """ http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'] def __init__(self, **kwargs): """ Constructor. Called in the URLconf; can contain helpful extra keyword arguments, and other things. """ # Go through keyword arguments, and either save their values to our # instance, or raise an error. for key, value in kwargs.items(): setattr(self, key, value) @classonlymethod def as_view(cls, **initkwargs): """Main entry point for a request-response process.""" for key in initkwargs: if key in cls.http_method_names: raise TypeError("You tried to pass in the %s method name as a " "keyword argument to %s(). Don't do that." % (key, cls.__name__)) if not hasattr(cls, key): raise TypeError("%s() received an invalid keyword %r. as_view " "only accepts arguments that are already " "attributes of the class." % (cls.__name__, key)) def view(request, *args, **kwargs): self = cls(**initkwargs) self.setup(request, *args, **kwargs) if not hasattr(self, 'request'): raise AttributeError( "%s instance has no 'request' attribute. Did you override " "setup() and forget to call super()?" % cls.__name__ ) return self.dispatch(request, *args, **kwargs) view.view_class = cls view.view_initkwargs = initkwargs # take name and docstring from class update_wrapper(view, cls, updated=()) # and possible attributes set by decorators # like csrf_exempt from dispatch update_wrapper(view, cls.dispatch, assigned=()) return view def setup(self, request, *args, **kwargs): """Initialize attributes shared by all view methods.""" if hasattr(self, 'get') and not hasattr(self, 'head'): self.head = self.get self.request = request self.args = args self.kwargs = kwargs def dispatch(self, request, *args, **kwargs): # Try to dispatch to the right method; if a method doesn't exist, # defer to the error handler. Also defer to the error handler if the # request method isn't on the approved list. if request.method.lower() in self.http_method_names: handler = getattr(self, request.method.lower(), self.http_method_not_allowed) else: handler = self.http_method_not_allowed return handler(request, *args, **kwargs) def http_method_not_allowed(self, request, *args, **kwargs): logger.warning( 'Method Not Allowed (%s): %s', request.method, request.path, extra={'status_code': 405, 'request': request} ) return HttpResponseNotAllowed(self._allowed_methods()) def options(self, request, *args, **kwargs): """Handle responding to requests for the OPTIONS HTTP verb.""" response = HttpResponse() response['Allow'] = ', '.join(self._allowed_methods()) response['Content-Length'] = '0' return response def _allowed_methods(self): return [m.upper() for m in self.http_method_names if hasattr(self, m)]

We could read this class from top to bottom, but it’s probably more helpful to follow the data. I notice that View.as_view() is something I see quite frequently in within urls.py files.

For example, in a Slack app I’m building, I have this line of code.

urlpatterns = [ path("events/", Events.as_view()), ]

Events is the name of a class-based view, which means that .as_view() is referencing or overriding View.as_view() .

Since we know that urls.py files call View.as_view() as an entry point, this seems like a good entry point for us as well.

@classonlymethod def as_view(cls, **initkwargs):

The method signature has a few interesting details worth noting. First, it uses the @classonlymethod decorator from django.utils.decorators . As the name suggests, this makes sure that the method is only called on the class directly, not on an instance of the class. If called on an instance, @classonlymethod throws an exception. This makes sense if we consider the urls.py code from above. .as_view() is called directly on the Event view, not on an object instance.

Note that the first parameter of as_view is cls , rather than self , which makes sense for a method that can only be called directly on a class.

Finally, **initkwargs is a dictionary of keyword arguments.

Moving on.

for key in initkwargs: if key in cls.http_method_names: raise TypeError("You tried to pass in the %s method name as a " "keyword argument to %s(). Don't do that." % (key, cls.__name__)) if not hasattr(cls, key): raise TypeError("%s() received an invalid keyword %r. as_view " "only accepts arguments that are already " "attributes of the class." % (cls.__name__, key))

Errors are often a useful way of understanding how a piece of code is used and misused. This for loop iterates over the initkwargs and checks the keys to make sure they’re valid.

It references cls.http_method_names , which is included in the code above:

http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']

So this first check is making sure that none of the keyword arguments in a urls.py file use an HTTP verb for a name. This behavior makes sense, as those http_method_names have existing purposes that developers shouldn’t override via keyword args.

The next check ensures that all keys match class-level attributes, even though we don’t see any class-level attributes defined on View , except for cls.http_method_names . Presumably, classes that inherit from View can define their own class-level attributes, which can, in turn, be overridden with initkwargs .

What follows is the most interesting piece of code in base.py .

def view(request, *args, **kwargs): self = cls(**initkwargs) self.setup(request, *args, **kwargs) if not hasattr(self, 'request'): raise AttributeError( "%s instance has no 'request' attribute. Did you override " "setup() and forget to call super()?" % cls.__name__ ) return self.dispatch(request, *args, **kwargs) view.view_class = cls view.view_initkwargs = initkwargs # take name and docstring from class update_wrapper(view, cls, updated=()) # and possible attributes set by decorators # like csrf_exempt from dispatch update_wrapper(view, cls.dispatch, assigned=()) return view

The method view() is defined within .as_view() . As you can see from the bottom of this snippet, the view() function will get returned from .as_view() to its caller, which in the case of urls.py means that view() will be passed as an argument to the path() method.

It’s not clear how path() makes use of view() , but we do know that view() isn’t executed inside of .as_view() . That’s important to keep in mind because it means the code from the snippet above won’t all be executed at the same time.

If we look inside of view() , we’ll see the first instantiation of the View class that we’ve come across.

self = cls(**initkwargs)

This is sort of tricky. Normally, when you see self in Python, it’s the name of a method argument that refers to the object itself. That’s not what’s going on here. In this case, self is just the name of a variable that is local to the scope of the view() method. self is used to store a new instance of the View class.

Just to reiterate in case this is confusing (as it was confusing to me, at first):

.as_view() is called at the class level, meaning it’s not tied to an object instance. When .as_view() gets called, it returns the entire view() function. When view() gets called, later on, it creates a new instance of the View class and assigns that value to self .

Closures

You might wonder how the view() function still has access to initkwargs , given that it isn’t passed in as an argument to view() , and the .as_view() method has already returned by the time the code in view() executes.

This is something known as a closure. The basic idea is that view() has access to the variables that were available in its local scope when it was first defined. This allows for some interesting software design opportunities.

Closures make it possible to effectively make information private. When path() gets passed the view() method, path() doesn’t have access to initkwargs , because that variable is out of scope for path() . This lets view() access variables that are hidden from path() .

On a related note, this ensures that values can be passed from .as_view() to .view() without telling the client code to do so. This lets path() worry only about the public API for view() while ensuring that the necessary values are available.

A full dive into closures is outside the scope of this article, so if you find this idea interesting (or infuriating), I recommend reading up on the subject, because it’s common in many programming languages.

Function-Based vs. Class-Based Views

Before diving back into the details of view() , let’s talk about function-based views vs. class-based views. I’ve always wondered how class-based views were treated by Django in comparison to function-based views.

Conceptually, function-based views are simple. Here’s a simple function-based view that returns an HTML snipped displaying the current time. The view itself receives a Request object and returns an HttpResponse .

def index_view(request): now = datetime.datetime.now() html = "<html><body>It is now %s.</body></html>" % now return HttpResponse(html)

The corresponding URL pattern matches a path ( home/ ) to the view method.

urlpatterns = [ path("home/", index_view), ]

It’s easy to picture Django with a key-value store that matches home/ to the index_view() function. When a request enters Django at the home/ path, it looks up index_view() and calls it with the Request object passed in.

Until now, I’ve never been quite clear how this is handled with class-based views. If we return to the initial urls.py file, we can see that it isn’t obvious what gets passed into path() , since the actual argument is whatever value gets returned from .as_view() .

urlpatterns = [ path("events/", Events.as_view()), ]

With the view() function, that question has been answered. When a request enters Django at events/ , Django calls view() and passes it the Request object.

Now back to the nested view() function to see what happens when that request gets received.

def view(request, *args, **kwargs): self = cls(**initkwargs) self.setup(request, *args, **kwargs) if not hasattr(self, 'request'): raise AttributeError( "%s instance has no 'request' attribute. Did you override " "setup() and forget to call super()?" % cls.__name__ ) return self.dispatch(request, *args, **kwargs)

That first line of code, self = cls(**initkwargs) , makes a call to View.__init__() .

def __init__(self, **kwargs): """ Constructor. Called in the URLconf; can contain helpful extra keyword arguments, and other things. """ # Go through keyword arguments, and either save their values to our # instance, or raise an error. for key, value in kwargs.items(): setattr(self, key, value)

This behavior is straightforward. It receives the values that were first passed to .as_view() as initkwargs and stores them as instance attributes.

That description, “Go through keyword arguments, and either save their values to our instance, or raise an error.”, appears to be slightly incorrect, as other methods handle raising errors if the keyword arguments are invalid.

The rest of view() calls .setup() , makes sure the instance has a .request attribute, and finally calls .dispatch() , returning the result.

Before looking into .setup() and .dispatch() , let’s take a look at the last few lines of .as_view() .

view.view_class = cls view.view_initkwargs = initkwargs # take name and docstring from class update_wrapper(view, cls, updated=()) # and possible attributes set by decorators # like csrf_exempt from dispatch update_wrapper(view, cls.dispatch, assigned=()) return view

This snippet starts by setting the .view_class and .view_initwkargs arguments on the view method. This seems strange because it’s not clear to me if these values are actually used in Django. A quick search in GitHub doesn’t point to any usages outside of tests and documentation. If you know the purpose of the .view_class and .view_initkwargs attributes, I’d love to hear.

It’s especially curious since view() doesn’t make use of .view_initkwargs , instead using the initkwargs dictionary passed into .as_view() .

The calls to update_wrapper() are responsible for overwriting the metadata for the view() function. This is common when a function is wrapped by another function, as is frequent when using decorator functions. To avoid returning the metadata of the wrapper function, the metadata of the wrapped function is copied over.

View.setup()

The view() function called the .setup() method, so this seems like a solid place to look next.

def setup(self, request, *args, **kwargs): """Initialize attributes shared by all view methods.""" if hasattr(self, 'get') and not hasattr(self, 'head'): self.head = self.get self.request = request self.args = args self.kwargs = kwargs

Another fairly simple method. First, it checks whether there is a .get attribute but no .head attribute, and if so, assigns self.head to self.get . This makes sense, as an HTTP HEAD request returns the headers that would get returned from a full GET request. Presumably, this is just a convenience for programmers who override the .get() functionality, but forget to add .head() functionality. It’s a way for Django to guarantee that all endpoints that support GET also support HEAD.

The final three lines just take the arguments passed into the view() function and assign them to attributes of the View instance.

View.dispatch()

.dispatch() is responsible for handling the actual HTTP request and sending it to the proper method for that HTTP verb.

def dispatch(self, request, *args, **kwargs): # Try to dispatch to the right method; if a method doesn't exist, # defer to the error handler. Also defer to the error handler if the # request method isn't on the approved list. if request.method.lower() in self.http_method_names: handler = getattr(self, request.method.lower(), self.http_method_not_allowed) else: handler = self.http_method_not_allowed return handler(request, *args, **kwargs)

Remember http_method_names from the top of View ?

http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']

.dispatch() gets a request object, and checks whether the request.method attribute matches one of those method names. If so, it looks for a method of View (or more likely, one of its children) with the same name. So when a GET request is received, .dispatch() will look for a .get() method.

If there isn’t a matching method name, then .http_method_not_allowed() is used instead. Looking below, we can see that this method just logs a warning about an invalid response, and returns an HttpResponseNotAllowed object with a list of allowed methods.

def http_method_not_allowed(self, request, *args, **kwargs): logger.warning( 'Method Not Allowed (%s): %s', request.method, request.path, extra={'status_code': 405, 'request': request} ) return HttpResponseNotAllowed(self._allowed_methods())

The .get() method, or whichever is appropriate for the particular request, is the closest comparison to the function in a function-based view. While function-based views are called directly when a request is received, for class-based views, the view() method gets called, which in turn calls .get() or another method.

Let’s look at the final two methods in View .

def options(self, request, *args, **kwargs): """Handle responding to requests for the OPTIONS HTTP verb.""" response = HttpResponse() response['Allow'] = ', '.join(self._allowed_methods()) response['Content-Length'] = '0' return response def _allowed_methods(self): return [m.upper() for m in self.http_method_names if hasattr(self, m)]

.options() is the only explicit declaration of a method handler on View . Developers can add as many HTTP-specific methods as they want, and Django will helpfully gather those method names for use in the .options() method.

Finally, ._allowed_methods() is a helper method used in .options() and .http_method_not_allowed() . It returns a list of available method names formatted in uppercase to match standard formatting for HTTP verbs.

Parting thoughts on View

By far, the most interesting thing I learned from reading this class is that View depends on methods whose names match the corresponding HTTP verbs to handle requests. I’m glad to have that piece of information included in my mental model of Django.

I’m also pleased to see that .as_view() functions by providing a callback. It makes the code a little less readable, but from the client’s standpoint, it makes views much easier to work with. View is an elegant abstraction that includes everything necessary to create a class-based view while giving Django the same interface as function-based views.

RedirectView

For our next class, let’s look at RedirectView . It’s a useful example because View is its only ancestor, and it provides a clear picture of how Django’s views are built on top of one another. It also provides little new functionality, as its sole purpose is to redirect requests to a different endpoint.

Feel free to skim the source code in its entirety.

class RedirectView(View): """Provide a redirect on any GET request.""" permanent = False url = None pattern_name = None query_string = False def get_redirect_url(self, *args, **kwargs): """ Return the URL redirect to. Keyword arguments from the URL pattern match generating the redirect request are provided as kwargs to this method. """ if self.url: url = self.url % kwargs elif self.pattern_name: url = reverse(self.pattern_name, args=args, kwargs=kwargs) else: return None args = self.request.META.get('QUERY_STRING', '') if args and self.query_string: url = "%s?%s" % (url, args) return url def get(self, request, *args, **kwargs): url = self.get_redirect_url(*args, **kwargs) if url: if self.permanent: return HttpResponsePermanentRedirect(url) else: return HttpResponseRedirect(url) else: logger.warning( 'Gone: %s', request.path, extra={'status_code': 410, 'request': request} ) return HttpResponseGone() def head(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) def post(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) def options(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) def delete(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) def put(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) def patch(self, request, *args, **kwargs): return self.get(request, *args, **kwargs)

The first couple of lines define class-level attributes. If you recall from View.as_view() , there was a test to ensure that all **initkwargs passed in from urls.py match predefined class-level attributes. There weren’t any examples of this in View , but now we’re seeing the first case of a view adding additional attributes that can be overridden via **initkwargs . It would be possible to call RedirectView.as_view(permanent=True) from urls.py or any other client.

class RedirectView(View): """Provide a redirect on any GET request.""" permanent = False url = None pattern_name = None query_string = False

While you can probably infer the purpose of each attribute from its name, there isn’t much else to examine until we see them in action.

The next method is .get_redirect_view() , but that appears to be used only internally, so let’s look at .get() , which will call .get_redirect_view() . Remember, as we saw in View , .get() is the method that will be dispatched to handle any request that comes in via GET request.

def get(self, request, *args, **kwargs): url = self.get_redirect_url(*args, **kwargs) if url: if self.permanent: return HttpResponsePermanentRedirect(url) else: return HttpResponseRedirect(url) else: logger.warning( 'Gone: %s', request.path, extra={'status_code': 410, 'request': request} ) return HttpResponseGone()

Right away we see that .get() calls .get_redirect_url() , and passes it’s own *args and **kwargs arguments through. After getting the new URL, Django makes use of one of the predefined class-level attributes, self.permanent , and depending on that value, it returns either HttpResponsePermanentRedirect or HttpResponseRedirect . Note that .get() is only handling the mechanics of choosing where to redirect; the actual redirection logic is abstracted behind various response objects.

In the event that no redirect URL is found, Django logs a warning and return HttpResponseGone .

If we look at the rest of the verb-based methods, we’ll see a consistent pattern of calling .get() .

def head(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) def post(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) def options(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) def delete(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) def put(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) def patch(self, request, *args, **kwargs): return self.get(request, *args, **kwargs)

This is a great example of why I love reading framework code. It’s easy to think of frameworks as magical things that are impossible to understand, but right under the surface, they’re just normal code written by skilled developers. In this case, all of the redirect behavior is identical for each HTTP verb, so all of the other methods call .get() and return the response.

Finally, let’s look at the code for .get_redirect_url .

def get_redirect_url(self, *args, **kwargs): """ Return the URL redirect to. Keyword arguments from the URL pattern match generating the redirect request are provided as kwargs to this method. """ if self.url: url = self.url % kwargs elif self.pattern_name: url = reverse(self.pattern_name, args=args, kwargs=kwargs) else: return None args = self.request.META.get('QUERY_STRING', '') if args and self.query_string: url = "%s?%s" % (url, args) return url

Django lets you define a redirect target by either defining a URL explicitly or giving a pattern name that Django will perform a reverse-lookup upon to find the matching URL. Either way, it will pull the *args and **kwargs from the original URL and attempt to insert them into the new URL.

It then does the same thing with any query parameters from the original URL. This functionality is especially helpful if you’ve simply changed the pattern for how you create URLs. For example, if your API endpoints were initially prefixed with api/ , but then you add versioning so they now start with api/v1/ , Django can redirect the request while maintaining all of the request data. It’s trivial to redirect example.com/api/blogs/3?order=date&reverse=true to example.com/api/v1/blogs/3?order=date&reverse=true .

TemplateView

Thus far, we’ve looked at two fully-fledged classes, but this wouldn’t be Django if it didn’t make considerable use of mixins. I often find that mixins make code harder to read, but thankfully, in this case, we can step through each piece without needing to keep other pieces in mind.

The TemplateView renders and returns templates. As you can see below, it does this via a .get() method.

class TemplateView(TemplateResponseMixin, ContextMixin, View): """ Render a template. Pass keyword arguments from the URLconf to the context. """ def get(self, request, *args, **kwargs): context = self.get_context_data(**kwargs) return self.render_to_response(context)

All of the functionality is handled by mixins. TemplateView calls .get_context_data() from ContextMixin to create the appropriate context data based on the **kwargs from the request. The context is passed to .render_to_response() , which is provided by TemplateResponseMixin .

Let’s now take a look at ContextMixin so we can see how context data is gathered before sending it to the template.

ContextMixin

class ContextMixin: """ A default context mixin that passes the keyword arguments received by get_context_data() as the template context. """ extra_context = None def get_context_data(self, **kwargs): kwargs.setdefault('view', self) if self.extra_context is not None: kwargs.update(self.extra_context) return kwargs

.get_context_data() is itself quite simple. It receives **kwargs , adds a few extra keys, returns `** kwargs’ to the caller.

kwargs.setdefault('view', self) is a dictionary method that doesn’t change anything if view is already in kwargs; otherwise, it adds view as a key with self (the instance of TemplateView ) as the value.

We can infer that extra_context is a way for classes that use TemplateView to add additional information to the context dictionary before it is passed to the template.

Finally, let’s take a look at TemplateResponseMixin , which we assume is responsible for receiving a template name, populating that template with the provided context information, and somehow formatting this as a response.

TemplateResponseMixin

Give the class a quick skim and we’ll look at the different pieces.

class TemplateResponseMixin: """A mixin that can be used to render a template.""" template_name = None template_engine = None response_class = TemplateResponse content_type = None def render_to_response(self, context, **response_kwargs): """ Return a response, using the `response_class` for this view, with a template rendered with the given context. Pass response_kwargs to the constructor of the response class. """ response_kwargs.setdefault('content_type', self.content_type) return self.response_class( request=self.request, template=self.get_template_names(), context=context, using=self.template_engine, **response_kwargs ) def get_template_names(self): """ Return a list of template names to be used for the request. Must return a list. May not be called if render_to_response() is overridden. """ if self.template_name is None: raise ImproperlyConfigured( "TemplateResponseMixin requires either a definition of " "'template_name' or an implementation of 'get_template_names()'") else: return [self.template_name]

Just like we did before, let’s start by looking at the class attributes.

class TemplateResponseMixin: """A mixin that can be used to render a template.""" template_name = None template_engine = None response_class = TemplateResponse content_type = None

The attribute names are fairly descriptive. Note that TemplateResponse is the default response class. I’ve never overridden template_engine , but it’s interesting to see that Django supports multiple levels of customization.

Now I’m curious what actually happens inside of .render_to_response() .

def render_to_response(self, context, **response_kwargs): """ Return a response, using the `response_class` for this view, with a template rendered with the given context. Pass response_kwargs to the constructor of the response class. """ response_kwargs.setdefault('content_type', self.content_type) return self.response_class( request=self.request, template=self.get_template_names(), context=context, using=self.template_engine, **response_kwargs )

The first thing I notice is that the method signature mentions **response_kwargs , which TemplateView doesn’t explicitly use. **response_kwargs is merged with content_type information (which by default is None ), and passed into the .__init__() method of the response class as keyword arguments.

This is somewhat curious behavior. It’s not clear to me why content_type is added to **response_kwargs , if **response_kwargs is passed into the response class (which by default is TemplateResponse ) as keywords.

Otherwise, most of the arguments are straightforward. It looks like .render_to_response() follows a similar design to RedirectView.get() . It is only concerned with marshaling the proper data, and it depends on the response class to handle the underlying mechanics.

.render_to_response() calls .get_template_names() , which is our final method. Let’s take a look.

def get_template_names(self): """ Return a list of template names to be used for the request. Must return a list. May not be called if render_to_response() is overridden. """ if self.template_name is None: raise ImproperlyConfigured( "TemplateResponseMixin requires either a definition of " "'template_name' or an implementation of 'get_template_names()'") else: return [self.template_name]

The default behavior involves returning the template name inside of a list; otherwise, it raises an exception if no template name has been configured. In future posts, I’ll dive into other generic views that Django provides, and I’m curious to see how other classes make use of this method. This method appears to support multiple template names, which makes sense for nested templates.

Parting Thoughts

Why does this matter? It’s easy to let your eyes glaze over as you’re looking at functions that seem to either modify a dictionary or raise an exception without doing anything more exciting.

But now that we know all of the attributes used in TemplateView , we have the information necessary to use TemplateView in our own code and understand exactly what’s happening.

Let’s override a few of these attributes for a pretend views.py file.

class HomeView(TemplateView): extra_context = {'currency': '$'} template_name = 'home.html'

While this doesn’t look like much, we have enough understanding to create a useful view, add additional context, and send that data to the specified template. It’s important to keep in mind how the code we’re reading fits into Django projects.

Hopefully, you have a better understanding of how class-based views fit together, and can picture how other classes build on top of this base-level of functionality. In future posts will look into this exact subject and explore the rest of Django’s generic class-based views.

Share this: Twitter

Facebook

