Comprehending Class-Based Views in Django - The View Base Class
Class-Based Views, or CBVs, are one of the most debated features of Django. Compared to their counterparts, Function-Based Views (FBVs), CBVs can seem more confusing and harder to understand. In this series, Comprehending Class-Based Views in Django, we'll walk through CBVs in detail to understand how they work and how to use them.
To start off, we will go over the View
base class, how a CBV is used in the URLconf, and how the View
class routes the view logic for other classes that inherit from it.
Prerequisites
This article is aimed at those who may have tried using CBVs before and want to understand more about how they work. You will get the most from this article if you:
- Have built a project with Django before
- Have tried using a CBV at least once
- Have a basic understanding of classes in Python
This article contains many incomplete code snippets that are used for illustrative purposes. If you have used Django before, understanding where these snippets fit into a project shouldn't be difficult. I've tried to add as much context as possible, but have left off much of the supporting code to keep the article to a digestible length.
How a CBV is called
Let's start by looking at how we would use a CBV in the URLconf compared to a FBV. Assuming we have a CBV called MyView
and a FBV called my_view
, in a project's urls.py
file, we use them in the path()
function as a part of urlpatterns
.
# Class-Based View
path('new-cbv/', MyView.as_view(), name='new_cbv')
# Function-Based View
path('new-fbv/', my_view, name='new_fbv')
Django expects the second argument to path()
to be a function. This means we can directly provide a FBV to path()
. We provide my_view
, and not my_view()
, because we don't want to call the function. Django will call it later and use it appropriately.
Using a CBV is different. At first, we might expect that we could just pass the MyView
class directly to path()
.
# Wrong way to use a CBV in the URLconf
path('new-cbv/', MyView, name='new_cbv')
This won't work though because path()
isn't expecting a class as an argument for the view. We need to somehow get a function from the class. We do this by calling .as_view()
on MyView
. But with MyView.as_view()
we are calling a function, not passing one directly like we did with the FBV. We don't know what .as_view()
returns yet, but if path()
expects a function, then .as_view()
must return one. To find out what it does return we have to dive into Django's built-in View
base class.
Diving into the View class
All CBVs inherit from View
as their base class. A CBV may inherit from many different classes and mixins, but they all start with View
. Let's look at the code behind View
straight from the Django source.
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(
'The method name %s is not accepted as a keyword argument '
'to %s().' % (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)]
There is a lot going on here, but we will work through it the way Django does starting with .as_view()
.
The as_view() method
Since we are calling .as_view()
in the URLconf, it is the first part of View
that gets called before an instance of the class is created.
@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(
'The method name %s is not accepted as a keyword argument '
'to %s().' % (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
The @classonlymethod
decorator is used with .as_view()
to make sure it isn't called on an instance of the class but instead is only called directly on the class. This is a good time to point out that we aren't going to directly create an instance of a class when using a CBV.
new_view = View() # You won't do this
Instead, an instance will be created later as a result of .as_view()
.
.as_view()
takes two arguments: cls
, which is the class .as_view()
is called on and is automatically passed to the method, and **initkwargs
. **initkwargs
are any keyword arguments that we pass to .as_view()
when calling it that may be needed when an instance of the class is finally created. We'll see another set of keyword arguments soon, so keep in mind that **initkwargs
is used during class instantiation. If we did have keyword arguments, we would pass them in the URLconf like this:
# Passing example keyword arguments to .as_view
path('new-cbv/', MyView.as_view(kwarg1=new_kwarg_1, kwarg2=new_kwarg_2), name='new_cbv')
The first code executed when .as_view()
is called is to loop through initkwargs
and perform two checks.
for key in initkwargs:
if key in cls.http_method_names:
raise TypeError(
'The method name %s is not accepted as a keyword argument '
'to %s().' % (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))
Each key in initkwargs
is first checked against the http_method_names
attribute of the View
class which contains a list of the HTTP verbs.
http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']
If we tried to pass an HTTP verb as an argument to .as_view()
, we would get an error because it could cause a problem with execution of the view logic.
Each key is also checked to make sure it matches an already existing attribute of the class. The View
class doesn't have class attributes beside http_method_names
, so lets take a quick look at the first few lines of Django's built-in RedirectView
CBV.
class RedirectView(View):
"""Provide a redirect on any GET request."""
permanent = False
url = None
pattern_name = None
query_string = False
RedirectView
has multiple class attributes that we could set via keyword arguments on .as_view()
. But if we tried to pass a keyword argument that wasn't one of those listed, we would get an error message.
After checking initkwargs
, we get to the part of .as_view()
that ties us back to the original goal of figuring out what .as_view()
returns and hoping it is a function since path()
in the URLconf expects one.
A function, view()
, is defined in .as_view()
and if we jump to the bottom of .as_view()
, we can see that it returns this function.
So now we know that
path('new-cbv/', MyView.as_view(), name='new_cbv')
will look like
path('new-cbv/', view, name='new_cbv')
once MyView.as_view()
is evaluated, which looks the same as when an FBV is used. This means that view()
will receive the same arguments an FBV would when Django calls it. We won't go into what Django does to call a view and how it does it, but you can learn more in the Django docs.
Before we get into the detail of view()
, there are a couple remaining parts of MyView.as_view()
. First, class attributes on view
of view.view_class
and view.view_initkwargs
are set to the class of the view and initkwargs
respectively.
Finally, two calls to update_wrapper()
are made. This copies metadata from the class and the dispatch()
method to view.
With .as_view()
complete, we can now look at the view()
function and what it does.
Creating the class instance with view()
What exactly does view()
do when it is called? In short, it kicks off the chain of events that perform the view logic and ultimately return a response. Let's see how it does that.
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()
takes three arguments: request
, *args
, and **kwargs
. These are provided by Django when it calls the view with *args
and **kwargs
coming from the URL pattern. It's important to not confuse **kwargs
with initkwargs
we saw earlier. **initkwargs
come from the call to as_view()
while **kwargs
come from patterns matched in the URL.
The first thing view()
does is create an instance of the class by passing it **initkwargs
and assigning the instance to self
. If we go back to the beginning of the View
class, we can see that the class attributes are set in the __init__()
method with **initkwargs
being called **kwargs
locally.
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)
Once the class instance is created, the setup()
method is called which takes the same three arguments passed to view()
. It saves the arguments to the class instance which makes them available to all later methods of the 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
It also checks if self
has a get
attribute and a head
attribute. If it has get
but not head
, it creates head
and assigns get
to it.
After setup()
is called, there is one more check in the view()
function to make sure that self
has a request
attribute.
if not hasattr(self, 'request'):
raise AttributeError(
"%s instance has no 'request' attribute. Did you override "
"setup() and forget to call super()?" % cls.__name__
)
The error message that would be displayed if there is no request attribute gives a good explanation of why this check is necessary - the setup()
method could be overridden creating the possibility of the request attribute not being created on the instance. We'll save the "why" and "how" of overriding class methods for a later article, but it is a common occurrence when you are creating your own CBVs or modifying Django's built in generic CBVs.
After this check, view()
finally returns by calling the dispatch()
method.
Routing the view logic with dispatch()
dispatch()
is passed the same three arguments as view()
and does exactly what it's name implies - it dispatches the view to the correct logic branch based on the request type.
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)
It takes the HTTP method of the request, checks to make sure it is in the list of allowed methods stored in self.http_method_names
and then gets the corresponding method of the same name from self
and assigns it to handler
. For example, if a GET request is made, handler
will be assigned the get()
method of the class. Or if a POST request is made, handler
will be the post()
method of the class. If the request method is not in self.http_method_names
or if self
doesn't have the corresponding method, then handler
gets assigned the http_method_not_allowed
method which will return HttpResponseNotAllowed
. dispatch()
returns by calling whatever method is assigned to handler
with the request
, *args
, and **kwargs
arguments.
If we go back and look at the complete code for the View
class, we'll notice that there are no get()
or post()
methods, or methods for most of the other HTTP methods. It only contains a method options()
to handle the HTTP OPTIONS method.
So what happens when we need to handle a GET request?
At this point, we have reached the limit of the View
class. View
is not meant as a standalone CBV. It's used as a base class from which other CBVs inherit. Classes that inherit from View
will define get()
, post()
, and other methods necessary to handle the request. If we look again at RedirectView
we can see how it defines methods for all the HTTP method names.
# Inside RedirectView
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)
Other generic CBVs or custom CBVs that you create yourself need to have methods defined to handle the HTTP methods that you expect to be requested of the view.
Summary
View
has a lot going on and it isn't even a complete view, so let's recap what we've gone over.
- The
View
class sets up and kicks off the chain of events needed to properly handle the logic of a CBV - Calling the
as_view()
method on the view in the URLconf returns theview()
function which Django will use to handle the request - When Django calls
view()
, an instance of the class is created and thedispatch()
method is called dispatch()
routes to the appropriate method based on the request's HTTP methodView
is not a complete view but a base from which other CBVs inherit
In future articles, we'll get into the details of using class-based views, how to create your own, and the power of Django's built-in generic CBVs.
Helpful Resources
Questions, comments, still trying to figure it out? - Let me know!