skip to content

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%

answer

  1. string first, response second
  2. request enables context processors
  3. model, view name or URL
  4. Model, Manager or QuerySet

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.

solid answer

~40 s

`render(request, template_name, context, content_type, status, using)` calls `render_to_string()` with the request, so context processors run and the CSRF token is available, then wraps the string in an `HttpResponse`. `redirect(to, *args, permanent=False, **kwargs)` returns a 302, or a 301 when permanent; `to` may be an object with `get_absolute_url()`, a URL name reversed with the extra arguments, or a URL. `get_object_or_404(klass, *args, **kwargs)` accepts a model, `Manager` or `QuerySet`, calls `.get()`, and turns `DoesNotExist` into `Http404`, but lets `MultipleObjectsReturned` through as a 500. Passing a filtered queryset scopes the lookup, for example to the signed-in member's loans. `get_list_or_404()` filters into a list and raises `Http404` when it is empty.

go deeper

for a junior

Recall what each shortcut returns: an HttpResponse from render(), a redirect from redirect(), an object or Http404 from get_object_or_404().

for a middle

Explain the machinery: render_to_string with the request, resolve_url's three target kinds, and .get() with DoesNotExist mapped to Http404.

for a senior

Scope lookups with querysets so members cannot reach others' records, and know the edges: MultipleObjectsReturned, the dotted-name redirect trap, empty lists as 404.

for a principal

Weigh the convenience of shortcuts that couple views to templates and URLs against keeping domain code free of them, as the shortcuts module itself admits.

## `render()`: a template into a response `render(request, template_name, context=None, content_type=None, status=None, using=None)` does two things in one call: 1. It calls `django.template.loader.render_to_string(template_name, context, request, using=using)`. Because the **request is passed**, the template gets a request-aware context: the configured **context processors** run, so `request`, `user`, `messages` and the CSRF token are available. `template_name` may also be a list; the first template that exists wins. 2. It wraps the resulting string in an `HttpResponse` with the given `content_type` and `status`. So `render()` is exactly "`render_to_string()` with the request, then `HttpResponse`". Use `render_to_string()` directly when you need the string, for an email body or a fragment inside JSON. The old `render_to_response()`, which did not take the request, was removed in Django 3.0. ## `redirect()`: three kinds of target `redirect(to, *args, permanent=False, **kwargs)` returns a **302** redirect, or a **301** with `permanent=True`. It works out the `Location` through `resolve_url()`, which accepts: | `to` is | `redirect()` does | |---|---| | an object with `get_absolute_url()` (usually a model instance) | uses that method's result | | a URL name, with `*args` / `**kwargs` | calls `reverse()` with them | | a string starting with `./` or `../` | uses it as a relative URL | | any other string that fails to reverse | re-raises `NoReverseMatch`, unless the string contains `/` or `.`, in which case it is used as a URL | The last row hides a trap. `redirect("loans:mine")` with a typo in the name raises `NoReverseMatch`, which is good. But `redirect("loans.mine")`, a name mistyped with a dot, does not raise: it contains a `.`, so Django treats it as a relative URL and sends the browser to `loans.mine`. The mistake shows up as a 404 in the browser, not an error in the view. ## `get_object_or_404()` and `get_list_or_404()` `get_object_or_404(klass, *args, **kwargs)` accepts a **model class, a `Manager` or a `QuerySet`** and calls `.get()` on it with the remaining arguments: - a match returns the object; - `DoesNotExist` becomes `Http404` with the message "No Loan matches the given query."; - **`MultipleObjectsReturned` is not caught**: two matching rows raise it, and the request ends as a 500. Passing a `QuerySet` is the idiomatic way to **scope** the lookup. `get_object_or_404(Loan.objects.filter(member=request.user), pk=loan_id)` gives a member a 404 for someone else's loan instead of showing it, and a 404 rather than a 403 does not even confirm that the loan exists. `get_list_or_404(klass, *args, **kwargs)` calls `.filter()` instead, evaluates it into a **Python list**, and raises `Http404` if the list is empty. Because it returns a list, you cannot chain further `QuerySet` methods on the result, and an empty listing page, which is often a perfectly valid state, becomes a 404. Most list views are better served by an ordinary `filter()` and an "empty" message in the template. ## A library-loan example ```python from django.shortcuts import get_list_or_404, get_object_or_404, redirect, render from .models import Loan def loan_detail(request, loan_id): loan = get_object_or_404( Loan.objects.select_related("copy"), pk=loan_id, member=request.user ) return render(request, "loans/detail.html", {"loan": loan}) def renew(request, loan_id): loan = get_object_or_404(Loan, pk=loan_id, member=request.user) loan.renew() return redirect(loan) # uses Loan.get_absolute_url() def open_loans_for_title(request, title_id): loans = get_list_or_404(Loan, copy__title_id=title_id, returned_at__isnull=True) return render(request, "loans/open_for_title.html", {"loans": loans}) ``` ## Async variants Django 5.0 added `aget_object_or_404()` and `aget_list_or_404()` for `async def` views. They behave the same, using `aget()` and async iteration under the hood; the sync versions must not be called from an async view, because they run synchronous database queries. ## `status` and `content_type` The two optional arguments of `render()` are easy to forget: - **`status`** renders the same kind of template with a different code, for example `render(request, "loans/limit_reached.html", {"limit": 5}, status=409)` when a member is at the loan limit. - **`content_type`** renders non-HTML output from a template, such as a plain-text receipt with `content_type="text/plain; charset=utf-8"`. Without them, `render()` sends status 200 and the `DEFAULT_CHARSET`-based HTML content type. ## Summary - `render()` = `render_to_string()` with the request, wrapped in `HttpResponse`. - `redirect()` = `resolve_url()` plus a 302 or 301 response. - `get_object_or_404()` = `.get()` with `DoesNotExist` turned into `Http404`; multiple matches still crash. - `get_list_or_404()` = `.filter()` into a list, with an empty list turned into `Http404`.

  • Why can redirect('loans.mine') in Django send the browser to a broken page instead of raising an error?
    `redirect()` first tries `reverse()`. When that raises `NoReverseMatch`, it re-raises only if the string contains neither `/` nor `.`; otherwise it assumes the string is a URL. A mistyped name with a dot is therefore sent as the relative URL `loans.mine`, and the problem appears as a 404 in the browser.
  • Why pass a QuerySet rather than a model class to get_object_or_404() in a Django view?
    A queryset lets you scope and shape the lookup: `get_object_or_404(Loan.objects.filter(member=request.user), pk=loan_id)` returns 404 for other members' loans, and `select_related()` on it avoids a follow-up query. The shortcut accepts a model, a `Manager` or a `QuerySet` and calls `.get()` on whichever it receives.

saying these in an interview costs you the question

  • get_object_or_404 returns the first row when several match.
  • redirect() raises NoReverseMatch for any name it cannot reverse.
  • redirect() issues a permanent 301 by default.
  • get_list_or_404 returns a QuerySet you can keep filtering.
  • get_object_or_404 only accepts a model class.