In a Django ListView or DetailView, when do you set queryset versus override get_queryset(), and why is a class-level queryset safe to share?
answer
- fixed filter vs per-request filter
- class attribute built once
- a fresh copy each request
- overrides can drop ordering
basics
~20 sSet queryset for a filter fixed at import time; override get_queryset() when it depends on the request, user or URL. The class-level QuerySet is safe because the generic get_queryset() hands each request a copy via .all().
solid answer
~40 s`queryset = Listing.objects.filter(status="published")` is a fixed, lazy description built once when the class is defined. It is safe to share because the generic views never use it directly: their `get_queryset()` returns `self.queryset.all()`, a new QuerySet with its own result cache, so no request sees rows another request fetched. Override `get_queryset()` when the filter depends on the request: `self.kwargs["city"]`, `self.request.GET`, `self.request.user`. Start from `super().get_queryset()` so `model`, `queryset` and, in `ListView`, the `ordering` attribute keep working. If neither `model` nor `queryset` is set and `get_queryset()` is not overridden, Django raises `ImproperlyConfigured`. The trap in a class-level queryset is not sharing but freezing: a value such as `timezone.now()` inside the filter is computed once and cloned forever.
code
python · 12 linesfrom django.test import TestCase
from django.urls import reverse
from listings.models import Listing
class ListingIndexFreshnessTests(TestCase):
def test_new_listing_appears_on_next_request(self):
self.client.get(reverse("listing-list"))
Listing.objects.create(title="Canal loft", status="published")
response = self.client.get(reverse("listing-list"))
self.assertContains(response, "Canal loft")go deeper
Recall that queryset is for a fixed filter and get_queryset() is for filters that depend on the URL, the query string or the user.
Explain that the generic get_queryset() clones the class-level QuerySet with .all(), so each request evaluates its own copy, and why overrides should start from super().
Spot the frozen-value trap and the ordering loss in reviews, and route every lookup through get_queryset() so scoping applies to lists and single objects alike.
Standardise a shared queryset mixin per resource so list, detail and edit views cannot drift apart in what they expose.
## Three ways to say which objects A display generic view (`ListView`, `DetailView`) needs a set of objects to list or to search for one. There are three ways to supply it, checked in this order by the default `get_queryset()`: | You set | The view uses | Typical use | |---|---|---| | `queryset = ...` | A copy of that QuerySet | A fixed filter: only published listings | | `model = ...` only | `model._default_manager.all()` | Everything in the table | | Neither, no override | — | `ImproperlyConfigured`: define `model`, `queryset`, or override `get_queryset()` | If both `queryset` and `model` are set, `queryset` wins. ## Why a class-level queryset is safe A class attribute is created **once per process**, when the module is imported, and shared by every request. A mutable object shared that way is normally a bug. A QuerySet is mutable in one important sense: once evaluated, it **caches its rows**. Django's generic views defend against that. The default `get_queryset()` never returns `self.queryset` itself: - `SingleObjectMixin.get_queryset()` (used by `DetailView`) returns `self.queryset.all()`. - `MultipleObjectMixin.get_queryset()` (used by `ListView`) calls `.all()` when the attribute is a QuerySet, then applies `ordering`. `.all()` returns a **new QuerySet** with the same filters and an empty result cache. Each request evaluates its own copy, so no request reads rows cached by another. Django's reference documentation says it directly: `queryset` holds a mutable value, so call `.all()` on it or go through `get_queryset()`. Two consequences: - **Do not use `self.queryset` directly** in your own methods; call `self.get_queryset()`. - **A plain list is not protected.** `ListView` accepts any iterable, but only QuerySets are cloned; a list class attribute is the same object for every request. ## When to override get_queryset() Override it whenever the set of objects depends on something only known during the request: 1. **URL arguments** — `path("homes/<slug:city>/", ...)` and `filter(city__slug=self.kwargs["city"])`. 2. **Query parameters** — a `?bedrooms=3` filter from `self.request.GET`. 3. **The user** — an agent's own drafts, from `self.request.user`. 4. **Time** — listings available now, because `timezone.now()` must be evaluated per request. ```python from django.utils import timezone from django.views.generic import ListView from listings.models import Listing class CityListingView(ListView): model = Listing ordering = ["-listed_at"] def get_queryset(self): return ( super() .get_queryset() .filter(city__slug=self.kwargs["city"], available_from__lte=timezone.now()) ) ``` Starting from **`super().get_queryset()`** keeps the configured source (`model` or `queryset`) and, for `ListView`, applies `ordering`. An override that builds `Listing.objects.filter(...)` from scratch silently ignores `ordering`, and any `queryset` restrictions set on the class or passed to `as_view()`. ## The frozen-value trap Cloning copies the **filter**, including any value computed when the class body ran: ```python class AvailableNowView(ListView): queryset = Listing.objects.filter(available_from__lte=timezone.now()) # frozen ``` `timezone.now()` ran once at import. Every request's clone compares against that moment, so newly available listings never appear until a restart. The rule: a class-level `queryset` may contain only **constants**; anything relative to "now", the user or the URL belongs in `get_queryset()`. ## Where each kind of filter belongs | Filter depends on | Put it in | Why | |---|---|---| | Nothing (always "published") | Class-level `queryset` | Built once, cloned per request, easy to read | | URL kwargs or query string | `get_queryset()` | Only known during the request | | The user | `get_queryset()` | `self.request.user` exists only per request | | The current time | `get_queryset()` | `timezone.now()` must be evaluated per request | | A rule reused outside views | A custom manager or QuerySet method, called from `get_queryset()` | One definition for views, admin actions and scripts | ## get_queryset() feeds everything else - `ListView` uses it as `object_list`. - `DetailView`'s `get_object()` filters it by `pk` or `slug`, so restricting `get_queryset()` restricts which single objects can be found too. - The editing generics build on the same `get_object()`, so one well-scoped `get_queryset()` in a shared mixin covers list, detail, update and delete pages.
- Your get_queryset() override returns Listing.objects.filter(city=...) and the ordering attribute stopped working. Why?In `ListView`, `ordering` is applied inside `MultipleObjectMixin.get_queryset()`, after it resolves `queryset` or `model`. An override that never calls `super().get_queryset()` skips that code, so `ordering` is ignored. Start from `super().get_queryset()` and add your filter, or call `.order_by()` yourself.
- Why does queryset = Listing.objects.filter(available_from__lte=timezone.now()) show stale results even though the view clones it?`.all()` clones the filter, not the moment it was written. `timezone.now()` was evaluated once when the class body ran, so every clone compares against that fixed timestamp. Move the filter into `get_queryset()` so `now()` is evaluated on each request.
The class-level queryset is a recipe card pinned in the kitchen; get_queryset() photocopies it for each order, so every diner gets a dish cooked from a fresh copy. The recipe itself must not say 'use today's fish' with a date written in ink on the day it was pinned up.
saying these in an interview costs you the question
- Believing a class-level queryset caches rows across requests in ListView
- Reading self.queryset directly in helper methods instead of calling get_queryset()
- Overriding get_queryset() without super() and expecting ordering to apply
- Putting timezone.now() or request-dependent values in the class-level queryset
- Assuming a list assigned to queryset is copied per request like a QuerySet