In Django, how does a QueryDict handle a repeated key such as ?status=paid&status=refunded, and why can't a view modify request.GET?
answer
- a list behind every key
- indexing gives the last
- getlist for all values
- copy() before you change
basics
~20 sA QueryDict stores a list per key: indexing and get() return the last value, getlist() returns all of them. request.GET and request.POST are immutable, so changes raise AttributeError; call copy() to get a mutable version.
solid answer
~30 s`request.GET` and `request.POST` are `QueryDict` objects, a `MultiValueDict` that keeps a list of values per key. For `?status=paid&status=refunded`, `request.GET["status"]` and `.get("status")` return the **last** value, `"refunded"`, while `.getlist("status")` returns `["paid", "refunded"]`. A missing key with `[]` raises `MultiValueDictKeyError`, a `KeyError` subclass that becomes a 500, so optional parameters use `get()`. The request's instances are immutable: assignment, `pop()` or `update()` raise `AttributeError: This QueryDict instance is immutable`, because they record what the client sent. To build a modified query string, for a pagination link for example, call `copy()`, which returns a mutable deep copy, change it, then `urlencode()` it.
go deeper
Recall that indexing and get() return the last value, getlist() returns all, and that request.GET cannot be changed in place.
Explain the MultiValueDict structure, MultiValueDictKeyError, why the request's QueryDicts are immutable, and how copy() and urlencode() build new query strings.
Review views for silent last-value-wins bugs on repeated parameters, crashes from indexing optional keys, and in-place mutation attempts in middleware.
Push query and form parsing through forms or validation layers so repeated-key semantics and type conversion are handled once, not in every view.
## `QueryDict` in one paragraph `request.GET` and `request.POST` are instances of **`django.http.QueryDict`**, a subclass of `MultiValueDict`, which is itself a `dict` subclass that stores a **list of values per key**. Query strings and form bodies can legitimately repeat a key, as in `?status=paid&status=refunded` from a filter with several checkboxes ticked, and a plain dict would lose all but one value. ## Reading values For the request `/payments/?status=paid&status=refunded`: | Expression | Result | |---|---| | `request.GET["status"]` | `"refunded"`: the **last** value | | `request.GET.get("status")` | `"refunded"`; a missing key returns `None` or the given default | | `request.GET.getlist("status")` | `["paid", "refunded"]` | | `request.GET.getlist("missing")` | `[]` | | `request.GET["missing"]` | raises `MultiValueDictKeyError`, a subclass of `KeyError` | | `request.GET.dict()` | `{"status": "refunded"}`: one value per key | | `list(request.GET.lists())` | `[("status", ["paid", "refunded"])]` | Two consequences matter in views: - **Use `getlist()` for multi-select filters.** `get()` silently drops every value but the last. - **Prefer `get()` over indexing for optional parameters.** An uncaught `MultiValueDictKeyError` becomes a 500, not a 400, so `request.GET["page"]` on a URL without `?page=` crashes the view. ## Why it is immutable The `QueryDict` objects on the request are created **immutable**. Any mutating call, whether `request.GET["page"] = "2"`, `setlist()`, `pop()` or `update()`, raises `AttributeError: This QueryDict instance is immutable`. The data describes what the client sent; letting one piece of code rewrite it would make every later reader, from middleware to templates, see something the client never sent. ## `copy()` and building new query strings When you need a modified version, typically to build a pagination or filter link that keeps the other parameters, take a copy: ```python params = request.GET.copy() # mutable deep copy params["page"] = "2" # replaces all values for "page" params.setlist("status", ["paid"]) next_url = f"{request.path}?{params.urlencode()}" ``` - **`copy()`** always returns a **mutable** deep copy, whatever the original's state. - **`urlencode()`** serializes every value of every key back into a query string, repeats included. - **`QueryDict(mutable=True)`** creates an empty mutable instance, and `QueryDict("a=1&a=2")` parses a string. ## How the query string is parsed `request.GET` is built from the raw `QUERY_STRING` when first accessed, with a few rules that explain odd results: | Query string | `request.GET` | |---|---| | `?q=` | `{"q": [""]}`: blank values are kept, so `get("q")` returns `""`, not `None` | | `?flag` | `{"flag": [""]}`: a key without `=` also gets an empty string | | `?a=1&a=2&a=3` | `{"a": ["1", "2", "3"]}`, in the order sent | | `?name=J%C3%BCrgen` | percent-decoded using `request.encoding`, `DEFAULT_CHARSET` (`"utf-8"`) unless changed | | more than 1,000 parameters | `TooManyFieldsSent`, answered with a 400 | The last row is the `DATA_UPLOAD_MAX_NUMBER_FIELDS` setting, 1000 by default, which caps how many fields any `QueryDict` parse may create, GET or POST. It protects the server from requests crafted to make parsing expensive. Blank values catch people out: `if request.GET.get("q") is not None` is true for `?q=`, so a search view should test for an empty string as well. ## Where `QueryDict` also appears - `request.POST` for form-encoded and multipart POSTs, with the same API. - Form binding: `PaymentFilterForm(request.GET)` hands the whole `QueryDict` to a form, whose multi-value widgets call `getlist()` for you. - Tests: the test client builds query strings from dicts and lists, so `self.client.get("/payments/", {"status": ["paid", "refunded"]})` produces the repeated key. ## Pitfalls interviewers probe 1. **`dict(request.GET)` is not `request.GET.dict()`.** The `dict` constructor copies the underlying storage, giving `{"status": ["paid", "refunded"]}`, while the `.dict()` method gives one value per key, `{"status": "refunded"}`. 2. **Mutating in middleware.** Code that tries to "normalize" parameters in place hits the immutability error; attach a derived value to the request instead. 3. **Integer parsing.** Values are always strings; `int(request.GET.get("page", 1))` still raises `ValueError` on `?page=abc`, which needs handling. 4. **Last value wins silently.** A client sending `?amount=10&amount=1000` gets `"1000"` from `get()`; validate through a form when it matters.
- In Django, how do you build a 'next page' link that keeps every other query parameter from request.GET?Copy, modify, encode: `params = request.GET.copy()`, `params["page"] = str(page + 1)`, then `f"?{params.urlencode()}"`. `copy()` gives a mutable deep copy, assignment replaces every value for that key, and `urlencode()` writes repeated keys back out.
- In Django, what does request.GET.dict() return for ?status=paid&status=refunded?`{"status": "refunded"}`: one value per key, the last one, because `dict()` builds the result through normal item access. Use `lists()` or `getlist()` when every value matters.
saying these in an interview costs you the question
- request.GET['status'] returns the first value when a key repeats.
- request.GET.get() returns a list when a key has several values.
- A view can assign request.GET['page'] to rewrite the query for later code.
- request.GET['missing'] returns None when the key is absent.
- QueryDict.copy() returns another immutable QueryDict.