In Django, what do the shortcuts render(), redirect() and get_object_or_404() do under the hood, and what can each accept?
answer
- string first, response second
- request enables context processors
- model, view name or URL
- Model, Manager or QuerySet
basics
~20 srender() 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
Recall what each shortcut returns: an HttpResponse from render(), a redirect from redirect(), an object or Http404 from get_object_or_404().
Explain the machinery: render_to_string with the request, resolve_url's three target kinds, and .get() with DoesNotExist mapped to Http404.
Scope lookups with querysets so members cannot reach others' records, and know the edges: MultipleObjectsReturned, the dotted-name redirect trap, empty lists as 404.
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.