skip to content

In Django REST Framework generic views, when do you override get_queryset() and get_serializer_class(), and why not read self.queryset directly?

level: middleimportance: should knowfreq 50%

answer

  1. attributes are only defaults
  2. import-time class attribute
  3. QuerySet result cache
  4. as_view() guard raises

basics

~20 s

Override get_queryset() when the rows depend on the request, and get_serializer_class() when the data shape does. self.queryset is a shared class attribute; evaluating it would cache results across requests, so DRF clones it with .all() and raises RuntimeError on direct evaluation.

solid answer

~30 s

The mixins never read the attributes directly: `list()` and `get_object()` call `get_queryset()`, and `get_serializer()` calls `get_serializer_class()`. So request-dependent rows — the caller's own tickets, a URL kwarg, `select_related()` for the serializer — go in `get_queryset()`, and request-dependent shapes — a write serializer for `POST`, a staff serializer — go in `get_serializer_class()`. The `queryset` attribute is built once at import and shared by every request; a `QuerySet` caches its rows once evaluated. The default `get_queryset()` therefore returns `self.queryset.all()`, a fresh clone, and `APIView.as_view()` makes direct evaluation of the class attribute raise `RuntimeError`. Calling `self.get_queryset()` also honours any subclass override.

code

python · 23 lines
python
from rest_framework import generics, permissions

from .models import Ticket
from .serializers import TicketCreateSerializer, TicketSerializer


class TicketListCreate(generics.ListCreateAPIView):
    permission_classes = [permissions.IsAuthenticated]

    def get_queryset(self):
        return (
            Ticket.objects.filter(requester=self.request.user)
            .select_related("queue")
            .order_by("-created_at")
        )

    def get_serializer_class(self):
        if self.request.method == "POST":
            return TicketCreateSerializer
        return TicketSerializer

    def perform_create(self, serializer):
        serializer.save(requester=self.request.user)

go deeper

for a junior

Know that get_queryset() decides which rows and get_serializer_class() decides which serializer, and that both can use self.request.

for a middle

Explain that the class attribute is created at import and shared, that QuerySets cache once evaluated, and how .all() and the as_view() guard prevent cross-request leaks.

for a senior

Use get_queryset() as the single choke point for scoping and query tuning, since detail lookups go through it too, and catch direct self.queryset reads in review.

for a principal

Standardise hook responsibilities across the codebase so scoping, serializer choice and save-time behaviour each have one obvious home and reviews can check them mechanically.

## Two hooks every generic view calls Django REST Framework's (DRF) `GenericAPIView` does not read its `queryset` and `serializer_class` attributes directly when handling a request. The mixins call two methods instead: - **`get_queryset()`** — used by `list()` and, through `get_object()`, by `retrieve()`, `update()` and `destroy()`. - **`get_serializer_class()`** — used by `get_serializer()`, which every mixin action calls. The attributes are only the **defaults** those methods return. That makes the methods the place to put anything that depends on the current request. ## When to override get_queryset() Override it whenever the set of objects depends on **who is asking or what they asked for**: - scoping to the caller, as in a support desk where `/tickets/` shows only the requester's own tickets: `Ticket.objects.filter(requester=self.request.user)`; - reading a URL keyword, such as a queue id from `self.kwargs`, for a nested endpoint; - adding `select_related()`, `prefetch_related()` or `annotate()` for the fields the serializer will read — the DRF docs point here as the place to avoid N+1 queries in list views. Because detail actions look objects up **through** `get_queryset()`, scoping it also scopes retrieve, update and delete: another user's ticket id returns 404, not the ticket. ## Why not read self.queryset directly? A class attribute such as `queryset = Ticket.objects.all()` is created **once, at import time**, and shared by every request the process serves. A Django `QuerySet` caches its results after its first evaluation. If a request iterated the class-level queryset, the cached rows would be served to every later request — stale data, and possibly another user's data. DRF defends against this in two ways: 1. The default `get_queryset()` returns `self.queryset.all()` when the attribute is a `QuerySet`. `.all()` returns a fresh clone per request, with an empty result cache. 2. `APIView.as_view()` patches the class-level queryset so that **evaluating it directly raises `RuntimeError`**: "Do not evaluate the `.queryset` attribute directly, as the result will be cached and reused between requests. Use `.all()` or call `.get_queryset()` instead." Chaining such as `self.queryset.filter(...)` also clones and therefore works, but the convention is to always go through `self.get_queryset()`: it picks up any override a subclass adds, which a direct attribute read would silently skip. ## When to override get_serializer_class() Override it when the **shape of the data** depends on the request: | Situation | Typical return | |---|---| | Writes accept fewer fields than reads show | a write serializer for `POST`, a read serializer for `GET` | | Staff see internal notes, requesters do not | a staff serializer when `self.request.user.is_staff` | | List rows are slimmer than the detail view | a summary serializer on the collection view | Two cautions from DRF's own behaviour: - If the method branches on `self.request.method`, remember that the **browsable API** calls it while rendering its forms, with the request method overridden — a branch that assumes only one method can render the wrong form. - Always obtain instances with **`self.get_serializer(...)`**, not by calling the class yourself. `get_serializer()` injects the context from `get_serializer_context()` — `request`, `view` and `format` — which hyperlinked fields and many custom fields need. ## What happens when neither is set If a view has no `queryset` attribute and does not override `get_queryset()`, the default method fails an assertion naming the view class and telling you to set the attribute or override the method. The same applies to `serializer_class` and `get_serializer_class()`. These are programming errors caught on the first request, not client errors. ## Extending the context instead of swapping the class Sometimes the serializer is right but needs one more input — the caller's organisation, a feature flag, a precomputed lookup table. Override **`get_serializer_context()`**, call `super().get_serializer_context()` to keep `request`, `view` and `format`, and add your key. The serializer reads it from `self.context`. This is cheaper and clearer than a second serializer class that differs only in where one value comes from, and it keeps `get_serializer_class()` focused on genuinely different shapes. ## Choosing the hook — a quick map - Which rows? `get_queryset()`. - Which serializer? `get_serializer_class()`. - What extra context does the serializer need? `get_serializer_context()`, calling `super()` and adding keys. - What happens at save? `perform_create()` / `perform_update()` / `perform_destroy()`. - How is one object found? `lookup_field`, or `get_object()` for multi-field lookups. Keeping each concern in its own hook means the mixins' flow — validation, status codes, pagination, permission checks — stays exactly as DRF ships it.

  • In DRF, why does a class-level queryset = Ticket.objects.all() not leak one request's rows into the next?
    Because nothing evaluates the class attribute itself. The default `get_queryset()` returns `self.queryset.all()`, a new clone with an empty result cache, per request. And `APIView.as_view()` replaces the class-level queryset's fetch step so that any direct evaluation raises `RuntimeError` telling you to use `.all()` or `get_queryset()`.
  • Why can branching on self.request.method in get_serializer_class() misrender DRF's browsable API?
    The browsable API calls `get_serializer_class()` while rendering its HTML forms, with the request method overridden to the form's method. Code that assumes the real request method — or reads something only present on one method — can pick the wrong serializer for a form. The DRF docs flag this explicitly next to the method's description.

saying these in an interview costs you the question

  • self.queryset is re-created by Django for every request.
  • Iterating self.queryset directly silently returns fresh rows each time.
  • Instantiate the serializer class directly instead of calling get_serializer().
  • Scoping get_queryset() affects only the list endpoint, not detail lookups.
  • Override list() to swap serializers for reads and writes.