skip to content

Function-Based Handlers

A function view takes an HttpRequest and returns an HttpResponse, usually through render(), redirect() and get_object_or_404(). Interviewers ask when a plain function beats a class-based view.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

4

In Django, what must a function-based view accept and return, and what happens when it returns None?

level: juniorimportance: must knowfreq 58%

answer

  1. request first, URL values after
  2. converted keyword arguments
  3. raising counts as an answer
  4. a branch without return

basics

~20 s

A Django function view takes the HttpRequest first, plus the URL's captured values as keyword arguments, and returns an HttpResponse or raises an exception Django maps to one. Returning None makes Django raise ValueError, surfacing as a 500.

solid answer

~40 s

A function view is any callable Django calls as `view(request, **captured)`: the `HttpRequest` first, then the values the route captured, already converted, so `<int:copy_id>` arrives as `copy_id=42`, plus any extra `kwargs=` from `path()`. It must return an `HttpResponse` or a subclass (a rendered page, `JsonResponse`, a redirect), or raise an exception Django maps to a response, such as `Http404` or `PermissionDenied`. After the call, the handler checks the result: `None` raises `ValueError` saying the view "didn't return an HttpResponse object. It returned None instead.", which the user sees as a 500. The usual cause is one `if` branch without a `return`, or calling `render()` without returning it.

go deeper

for a junior

Recall the shape: request in, HttpResponse out, URL values as keyword arguments, and that returning None is an error, not an empty page.

for a middle

Explain how the handler calls the view, what it checks on the way out, and which exceptions it turns into 404, 403 and 400 responses.

for a senior

Spot the missing-return branch in review, keep URL parameter names aligned with route captures, and prefer raising mapped exceptions over threading statuses through helpers.

for a principal

Use the minimal contract as the anchor for team conventions: any callable qualifies, so consistency in how views are written matters more than the form they take.

## The whole contract A Django **view** is any Python callable that Django's request handler can call and that hands back a response. For a function view the contract has three parts: 1. **It accepts the request first.** Django calls it with an `HttpRequest` as the first positional argument. The parameter is called `request` by convention only. 2. **It accepts what the URL captured.** Values captured by named parts of the route arrive as **keyword arguments**, already converted by the path converter, plus any extra `kwargs=` given to `path()`. For `path("copies/<int:copy_id>/checkout/", views.checkout)`, a request for `/copies/42/checkout/` becomes `checkout(request, copy_id=42)`: an `int`, not the string `"42"`. 3. **It returns an `HttpResponse`, or raises.** Any subclass counts: a rendered page, a `JsonResponse`, a redirect, a streamed file. Instead of returning, it may raise an exception that Django knows how to answer, such as `Http404` or `PermissionDenied`. Nothing registers the function anywhere else. The URLconf references it, and that reference is the only wiring there is. ```python # loans/urls.py from django.urls import path from . import views app_name = "loans" urlpatterns = [ path("copies/<int:copy_id>/checkout/", views.checkout, name="checkout"), ] ``` ## What the handler checks After the view returns, Django's `BaseHandler` checks the result before doing anything else with it: | View returned | What Django does | |---|---| | an `HttpResponse` or subclass | passes it back out through the middleware | | a response with a `render()` method, such as a `TemplateResponse` | runs template-response middleware, then renders it | | `None` | raises `ValueError`: "The view loans.views.checkout didn't return an HttpResponse object. It returned None instead." | | an unawaited coroutine | raises `ValueError` suggesting you may need to add an `await` | The `None` case is the classic junior bug. It almost never comes from a view that returns nothing at all; it comes from **one branch** that forgets to: ```python from django.shortcuts import get_object_or_404, redirect, render from catalog.models import Copy def checkout(request, copy_id): copy = get_object_or_404(Copy, pk=copy_id) if request.method == "POST": if copy.is_available(): copy.lend_to(request.user) return redirect("loans:mine") # Bug: an unavailable copy falls off the end of the function. else: return render(request, "loans/confirm.html", {"copy": copy}) ``` A POST for a copy that is already on loan reaches the end of the function, Python returns `None`, and the member sees a 500 error page instead of a message. Writing `render(...)` without `return` produces the same error, because the rendered response is built and then discarded. ## Raising instead of returning A view does not have to build every error response itself. Django maps a few exceptions to responses: - **`Http404`** (from `django.http`), raised directly or by `get_object_or_404()`, becomes a 404 through `handler404`. - **`PermissionDenied`** (from `django.core.exceptions`) becomes a 403 through `handler403`. - **`BadRequest`** and unhandled `SuspiciousOperation` become a 400. - Anything else becomes a 500, shown as the technical page while `DEBUG` is on. That is why a helper deep inside a view can raise `Http404` and the view stays linear, with no status codes threaded back through return values. ## Things that are not part of the contract - **Returning a `dict` or a string.** Django never serializes a view's return value for you; a view that returns a dict fails because the value is not a response. Build a `JsonResponse` or an `HttpResponse` explicitly. - **Method restriction.** A plain function view receives every HTTP method. Restricting methods is up to the view's own `if` or a decorator. - **Naming.** Django does not care what the function is called or where it lives, as long as the URLconf can import it. - **Being a function.** Any callable works; class-based views satisfy the same contract because `as_view()` returns a function. ## Calling a view directly Because a function view is only a function, a test can call it without the URLconf or the test client. `django.test.RequestFactory` builds an `HttpRequest`; no middleware runs, so the test sets `request.user` itself: ```python from django.test import RequestFactory, TestCase from loans.views import checkout class CheckoutContractTests(TestCase): # self.member and self.lent_copy are created in setUpTestData(). def test_unavailable_copy_still_gets_a_response(self): request = RequestFactory().post("/copies/42/checkout/") request.user = self.member response = checkout(request, copy_id=self.lent_copy.pk) self.assertIsNotNone(response) ``` Called this way, the buggy view above simply returns `None`: the `ValueError` is raised by Django's handler after the call, not by the function. A direct-call test therefore has to assert that a response came back. ## Why interviewers ask it The question looks trivial and filters on precision: candidates who know the contract can explain the `None` error from its message alone, know that URL values arrive as converted keyword arguments, and know that raising `Http404` is a response strategy rather than a crash.

  • Why does Django not accept a dict returned from a function view as JSON?
    The contract is an `HttpResponse`, and Django never serializes a view's return value on its own. A dict is not a response, so the request fails. Return `JsonResponse(data)` instead, which serializes the dict and sets the JSON content type.
  • In Django, how do values captured by path('copies/<int:copy_id>/', view) reach the view?
    As keyword arguments named after the capture, already converted by the path converter: `view(request, copy_id=42)` with an integer. Extra `kwargs=` passed to `path()` arrive the same way. The view's parameter names must match the capture names, or the call fails with a `TypeError`.

saying these in an interview costs you the question

  • A Django view can return a dict and Django turns it into JSON.
  • URL captures arrive as positional strings that the view must convert.
  • A view must be registered with a decorator before the URLconf can use it.
  • Raising Http404 inside a view is an unhandled crash.
  • A function view only receives GET and POST requests by default.
open as a page

In Django, when does a plain function-based view beat a class-based view, and when does a generic class-based view earn its keep?

level: middleimportance: must knowfreq 72%

basics

~20 s

Function views win for bespoke flows, one-off endpoints and readability; generic class-based views win when a page matches a standard list, detail or form pattern or when many views share behaviour through mixins. Both satisfy the same request-in, response-out contract.

open as a page

In Django, what do the shortcuts render(), redirect() and get_object_or_404() do under the hood, and what can each accept?

level: middleimportance: should knowfreq 50%

basics

~20 s

render() runs render_to_string() with the request and wraps it in an HttpResponse; redirect() resolves a model, URL name or URL into a 302; get_object_or_404() calls .get() on a Model, Manager or QuerySet and turns DoesNotExist into Http404.

open as a page

A Django checkout view for library loans has grown to 150 lines of availability rules, fee checks and emails; how would you make it thin?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Keep only HTTP work in the view: read input, check access, call one domain function, choose the response. Move single-object rules into model or QuerySet methods and multi-model operations into a plain service function that takes no request and raises domain exceptions.

open as a page