How does Django's DetailView get_object() find its object using pk_url_kwarg, slug_url_kwarg and slug_field, and what happens when the lookup fails?
answer
- URL kwarg names vs model field
- primary key checked first
- slug ignored when pk present
- zero rows, one row, many rows
basics
~20 sDetailView.get_object() filters get_queryset() by the URL's pk (pk_url_kwarg) or, failing that, filters the slug_field column by the URL's slug (slug_url_kwarg), then calls .get(). No match gives 404; neither kwarg raises AttributeError; duplicate slugs raise MultipleObjectsReturned.
solid answer
~40 s`get_object()` starts from `get_queryset()`. It reads `self.kwargs[pk_url_kwarg]` (default `"pk"`) and `self.kwargs[slug_url_kwarg]` (default `"slug"`). A pk filters by primary key. A slug filters the model field named by `slug_field` (default `"slug"`), but only when no pk was captured, unless `query_pk_and_slug = True`, which requires both to match. With neither kwarg, it raises `AttributeError` saying the view must be called with a pk or a slug. It then calls `.get()`: `DoesNotExist` becomes `Http404`, but `MultipleObjectsReturned` is not caught, so a non-unique slug gives a 500. Rename URL kwargs with `pk_url_kwarg`/`slug_url_kwarg`, point at another column with `slug_field`, and override `get_object()` only when the lookup is not a single filter.
code
python · 22 linesfrom django.urls import path
from django.views.generic import DetailView
from listings.models import Property
class PropertyByReferenceView(DetailView):
model = Property
slug_url_kwarg = "ref"
slug_field = "reference_code"
context_object_name = "home"
class PropertyCanonicalView(DetailView):
model = Property
query_pk_and_slug = True
urlpatterns = [
path("homes/ref/<str:ref>/", PropertyByReferenceView.as_view()),
path("homes/<int:pk>/<slug:slug>/", PropertyCanonicalView.as_view()),
]go deeper
Recall that DetailView looks for pk or slug in the URL and returns 404 when no object matches.
Explain the order: pk first, slug only without a pk unless query_pk_and_slug, plus the difference between slug_url_kwarg and slug_field.
Anticipate the 500 from non-unique slugs and the ignored slug in pk-plus-slug URLs, and keep overrides of get_object() routed through get_queryset().
Choose a URL identity scheme for public pages that balances readable slugs, stable ids and resistance to enumeration.
## The three names involved `DetailView` (through `SingleObjectMixin`) separates **what the URL calls the value** from **which model field it is compared with**: | Attribute | Default | Meaning | |---|---|---| | `pk_url_kwarg` | `"pk"` | Name of the URL keyword argument holding the primary key | | `slug_url_kwarg` | `"slug"` | Name of the URL keyword argument holding the slug | | `slug_field` | `"slug"` | Name of the **model field** the slug is compared with | | `query_pk_and_slug` | `False` | Whether to filter by both pk and slug when both are captured | So a property page at `homes/<str:ref>/` that looks up `Property.reference_code` sets `slug_url_kwarg = "ref"` and `slug_field = "reference_code"`. There is no `pk_field`: the pk always filters the primary key. ## The algorithm, step by step 1. **Start from the scoped set**: `queryset = self.get_queryset()` (or a queryset passed in by a subclass). 2. **Read the URL values**: `pk = self.kwargs.get(self.pk_url_kwarg)` and `slug = self.kwargs.get(self.slug_url_kwarg)`. 3. **Filter by pk** if one was captured. 4. **Filter by slug** if one was captured **and** either there is no pk or `query_pk_and_slug` is `True`. The field comes from `get_slug_field()`, which returns `slug_field`. 5. **Neither captured**: raise `AttributeError`, "Generic detail view ... must be called with either an object pk or a slug in the URLconf." 6. **Fetch** with `queryset.get()`. ## How it fails | Situation | What happens | Status | |---|---|---| | No matching row | `DoesNotExist` caught, `Http404` raised with "No property found matching the query" | 404 | | Several matching rows | `MultipleObjectsReturned` is **not** caught | 500 | | URL captures `id` instead of `pk` | `AttributeError` from step 5 | 500 | | Neither `model` nor `queryset` set | `ImproperlyConfigured` from `get_queryset()` | 500 | The `MultipleObjectsReturned` row is the one that bites in production: if the `slug` field is not `unique=True`, two listings called "two-bed-flat-central" make both pages crash. Fix it at the model (unique slugs), or put the pk in the URL too and enable `query_pk_and_slug`. ## pk and slug together A common URL shape is `homes/<int:pk>/<slug:slug>/`, giving readable links with a stable id. Know what the default does with it: - With `query_pk_and_slug = False` (default), **the slug is ignored**. `/homes/42/anything/` finds listing 42, which means any slug works and several URLs serve the same page. - With `query_pk_and_slug = True`, both must match. A wrong slug gives 404, and Django's documentation notes this also makes sequential ids harder to enumerate, because each URL needs two correct values. Slugs need not be unique in this mode. If you want wrong slugs to redirect to the canonical URL instead of 404, keep the default and compare `self.object.slug` with the URL in `get()`, which is custom code, not a built-in option. ## The URLconf side of the lookup Before `get_object()` runs, the URL resolver has already done part of the work: - A **path converter** such as `<int:pk>` only matches digits, so `/homes/abc/` never reaches the view; the resolver returns 404 because no pattern matched. - `<slug:slug>` matches letters, digits, hyphens and underscores; anything else is again a resolver 404. - The captured values arrive in `self.kwargs` already converted, so `self.kwargs["pk"]` is an `int`. - Only the **names** in the pattern matter to `get_object()`. A converter called `listing_id` is invisible to it until `pk_url_kwarg = "listing_id"` says where to look. So a 404 on a detail page has two possible sources: no URL pattern matched, or `get_object()` found no row. With `DEBUG` on, the 404 page tells them apart: a view-raised 404 shows the "No property found matching the query" message and a "Raised by" line naming the view, while a resolver 404 reports that the path matched none of the URL patterns it tried. ## When to override get_object() Override `get_object()` only when the lookup is not a filter on the scoped QuerySet, for example the "current user's profile" page with no identifier in the URL. Two rules: - Derive from `self.get_queryset()` if you filter at all, so any scoping in `get_queryset()` still applies. - Keep the 404 contract: use `get_object_or_404()` or catch `DoesNotExist` and raise `Http404`. `get_object()` is called once, in `get()`, and stored as `self.object`; methods like `get_context_data()` should read `self.object` rather than calling `get_object()` again, which would repeat the query.
- Two listings share the slug 'garden-flat'. What does a slug-only DetailView do for that URL, and how do you fix it?`get_object()` filters by the slug and calls `.get()`, which raises `MultipleObjectsReturned`. The view only catches `DoesNotExist`, so the request fails with a 500. Make the slug field `unique=True` (with a migration that deduplicates existing rows), or put the pk in the URL and set `query_pk_and_slug = True` so the pair identifies one row.
- Why should get_context_data() on a DetailView read self.object instead of calling get_object()?`DetailView.get()` already called `get_object()` and stored the result as `self.object`. Calling it again repeats the database query and, if the data changed in between, could even return a different row than the one being rendered. Read `self.object`, which is also what the default `get_context_data()` uses.
saying these in an interview costs you the question
- Believing slug_field is the name of the URL keyword argument
- Thinking a DetailView returns 404 when several rows share a slug
- Assuming both pk and slug are checked by default when both are captured
- Capturing id in the URL and expecting DetailView to use it without configuration
- Overriding get_object() with Model.objects.get() and bypassing get_queryset()