In Django 5.2 and later, how do reverse()'s query and fragment arguments help build a redirect to a filtered job list?
answer
- new keyword-only arguments
- encoding done for you
- request.GET works directly
- anchor text is not encoded
- redirect() lacks them
basics
~10 sSince Django 5.2, reverse('job-list', query={'status': 'open', 'page': 2}, fragment='job-42') returns '/jobs/?status=open&page=2#job-42'. The query is URL-encoded, may be a QueryDict such as request.GET, and the fragment is appended as given.
solid answer
~40 sBefore 5.2 you reversed the path and glued on `'?' + urlencode(...)` and `'#...'` yourself. Since Django 5.2, `reverse()` and `reverse_lazy()` take keyword-only `query` and `fragment`. `query` accepts a `QueryDict`, such as `request.GET`, encoded with its own `urlencode()`, or anything `urllib.parse.urlencode` accepts, encoded with `doseq=True` so list values repeat the key. An empty query adds no `?`, and a `None` value is written as the text `None`. `fragment` is appended after `#` without encoding. `redirect()` has no such parameters: its extra keywords are passed to `reverse()` as URL arguments, so `redirect('job-list', query=...)` raises `NoReverseMatch`; build the URL with `reverse()` first and pass it to `redirect()`.
code
python · 6 linesfrom django.http import QueryDict
from django.urls import reverse
filters = QueryDict('status=open&tag=python&tag=django')
reverse('job-list', query=filters, fragment='results')
# '/jobs/?status=open&tag=python&tag=django#results'go deeper
Recall that since Django 5.2 reverse() can add a query string and a fragment for you through the query= and fragment= keywords.
Explain the accepted query inputs, QueryDict or urlencode-compatible values with doseq, the empty-query and None behaviour, and the unencoded fragment.
Build redirect targets with reverse() and pass the string to redirect(), preserve user filters via request.GET, and remove hand-rolled encoding during 5.2 upgrades.
Use features like this as an upgrade argument: moving to the current LTS removes whole classes of URL-building bugs from redirect code.
## The job-board scenario A candidate submits an application from a filtered job list, `/jobs/?status=open&tag=python`, and the site should send them back to the same filtered list, scrolled to the job they applied for. The redirect target therefore has three parts: the path from the route name, a **query string** and a **fragment**. ## Before and after Django 5.2 ```python from urllib.parse import urlencode from django.shortcuts import redirect from django.urls import reverse # Before 5.2: assemble by hand url = reverse('job-list') + '?' + urlencode({'status': 'open', 'tag': 'python'}) + '#job-42' # Django 5.2+: let reverse() do it url = reverse('job-list', query={'status': 'open', 'tag': 'python'}, fragment='job-42') # '/jobs/?status=open&tag=python#job-42' response = redirect(url) ``` The hand-built version is easy to get wrong: forgetting to encode values, adding `?` to an empty query, or joining with `&` when the path already had a query. ## How the arguments behave | Input | Result | |---|---| | `query={'status': 'open', 'page': 2}` | `?status=open&page=2` | | `query={'tag': ['python', 'django']}` | `?tag=python&tag=django`, because `doseq=True` | | `query=request.GET` (a `QueryDict`) | encoded with `QueryDict.urlencode()`, keeping repeated keys | | `query={}` | nothing appended, no bare `?` | | `query={'ref': None}` | `?ref=None`: `None` becomes text, it is not dropped | | `fragment='job-42'` | `#job-42`, appended without encoding | Key details: - Both arguments are **keyword-only**, so positional calls cannot pass them by accident. - `query` is encoded for you; spaces become `+` and reserved characters are percent-encoded. - `fragment` is **not** encoded; pass a value that is already safe, such as an element id. - `reverse_lazy()` accepts the same two arguments. ## Preserving the user's filters The most useful input is often the incoming request's own query string: ```python from django.shortcuts import get_object_or_404, redirect from django.urls import reverse from .models import Job def job_apply(request, pk): job = get_object_or_404(Job, pk=pk) ... # validate and save the application url = reverse('job-list', query=request.GET, fragment=f'job-{job.pk}') return redirect(url) ``` Passing `request.GET` keeps every filter the candidate had applied, including repeated keys such as several `tag` values, without copying them one by one. ## The redirect() trap `redirect(to, *args, **kwargs)` forwards its extra keyword arguments to `reverse()` as **URL keyword arguments**. So: 1. `redirect('job-list', query={'status': 'open'})` asks for a `job-list` route with a `query` parameter. 2. No such route exists, so `reverse()` raises `NoReverseMatch`. 3. `redirect()` re-raises it, because the name `'job-list'` contains no `/` or `.` and so does not look like a URL. The fix is always the two-step form: `redirect(reverse('job-list', query=...))`. ## Common mistakes and how to test them - **Adding `?` by hand after `reverse(..., query=...)`**, which produces `??` or a stray `?` when the query is empty. - **Passing user-controlled text as `fragment`**, which is appended unencoded; keep fragments to ids the code generates. - **Expecting `None` values to disappear.** They are written as the text `None`; drop such keys from the dictionary before reversing. - **Mutating `request.GET` to add a key.** A request's `QueryDict` is immutable; call `request.GET.copy()`, change the copy, then pass it as `query`. A test posts an application with the test client and asserts the `Location` header, for example that it equals `reverse('job-list', query={'status': 'open'}, fragment='job-42')`. Comparing against `reverse()` output rather than a hand-typed string keeps the test valid when the path changes. ## Templates and versions - The `{% url %}` tag has no query or fragment arguments; templates build query strings with other tags, such as `{% querystring %}`, added in Django 5.1. - On Django 5.1 or earlier, including older LTS lines, keep the manual `urlencode()` approach; passing `query=` there raises `TypeError` for an unexpected keyword. - On Django 5.2 LTS and 6.x, prefer the arguments: they remove a whole class of encoding bugs from redirect code.
- Why is Django's fragment argument not URL-encoded while query is?The query is built from keys and values that must be encoded to be valid, so Django encodes them. The fragment is appended verbatim after `#`, as Django's docs show with an unencoded example, so the caller is responsible for passing a safe value such as an element id rather than arbitrary user input.
- How would you write this job-list redirect on Django 4.2, before query= existed?Reverse the path, then append the parts yourself: `reverse('job-list') + '?' + request.GET.urlencode()` when the query is non-empty, plus `'#job-42'`. Guard against an empty query to avoid a bare `?`. Upgrading to 5.2 or later lets you replace this with `reverse(..., query=request.GET, fragment=...)`.
saying these in an interview costs you the question
- redirect('job-list', query={...}) passes the query through to reverse().
- reverse() with query={} still appends a bare question mark.
- Keys whose value is None are left out of the query string.
- reverse() percent-encodes the fragment the same way as the query.
- The query and fragment arguments have existed since reverse() was introduced.