In Django REST Framework generic views, when do you override get_queryset() and get_serializer_class(), and why not read self.queryset directly?
answer
- attributes are only defaults
- import-time class attribute
- QuerySet result cache
- as_view() guard raises
basics
~20 sOverride 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 sThe 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 linesfrom 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
Know that get_queryset() decides which rows and get_serializer_class() decides which serializer, and that both can use self.request.
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.
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.
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.