In Django, what must a function-based view accept and return, and what happens when it returns None?
answer
- request first, URL values after
- converted keyword arguments
- raising counts as an answer
- a branch without return
basics
~20 sA 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 sA 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
Recall the shape: request in, HttpResponse out, URL values as keyword arguments, and that returning None is an error, not an empty page.
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.
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.
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.