skip to content

In Django 5.1 and later, what does the {% querystring %} tag do for pagination links that must keep the current filters?

level: middleimportance: nice to knowfreq 24%

answer

  1. new in 5.1
  2. starts from request.GET
  3. keyword arguments override
  4. None removes a key

basics

~20 s

{% querystring %} rebuilds the query string from request.GET with changes: keyword arguments add or replace keys and None removes one, so page links keep active filters. Since Django 6.0 the output always starts with ?.

solid answer

~40 s

`{% querystring %}`, added in **Django 5.1**, outputs a URL-encoded query string built from `request.GET` by default. Keyword arguments set or replace keys, `None` removes a key, and a list value sets several values for one key, so `<a href="{% querystring page=page_obj.next_page_number %}">` keeps `?status=paid&sort=-date` intact and changes only `page`. It replaces the hand-written loop over `request.GET` that pagination templates used to need. Since **Django 6.0** the result always starts with `?`, even when empty, and the tag accepts several positional mappings, such as a `QueryDict` or `dict`, merged left to right before the keyword arguments. It reads `request.GET` from the context, so the `request` context processor must be enabled or a mapping passed explicitly.

code

django · 5 lines
django
{# URL: /invoices/?status=paid&sort=-date&page=2 #}
<a href="{% querystring page=3 %}">Next</a>
{# href="?status=paid&amp;sort=-date&amp;page=3" #}
<a href="{% querystring sort='date' page=None %}">Oldest first</a>
{# href="?status=paid&amp;sort=date" #}

go deeper

for a junior

Know that {% querystring page=... %} builds pagination links that keep the other query parameters, and that None removes a parameter.

for a middle

Explain the merge order, list values, the need for the request in the context, and the Django 6.0 change to always prefix a question mark.

for a senior

Replace hand-rolled query-string loops and custom tags with the built-in during upgrades, and reset pagination when filters change.

for a principal

Use upgrades to retire bespoke template utilities that the framework now provides, reducing custom code the team must maintain.

## The problem it solves A list page with filters and pagination has URLs like `/invoices/?status=paid&sort=-date&page=2`. The "next page" link must keep `status` and `sort` while changing `page`. Before Django 5.1 the usual template code looped over `request.GET`, skipped `page`, re-emitted every other key and value, and appended the new page number. It was verbose and easy to get subtly wrong with multi-valued keys and encoding. **`{% querystring %}`**, a built-in tag since **Django 5.1**, does this in one line: ```django <a href="{% querystring page=page_obj.next_page_number %}">Next</a> ``` ## How it builds the result The tag is a built-in `simple_tag` with `takes_context=True`. Step by step: 1. **Start** from `request.GET`, or from the positional mappings you pass. 2. **Apply** positional mappings in order, then the keyword arguments as a final mapping; later values win. 3. **Remove** any key whose value is `None`. 4. **Set lists** as multiple values for one key; `None` entries inside a list are dropped, and if all entries are `None` the key is removed. 5. **Encode** the result with `QueryDict.urlencode()` and prefix it with `?`. | Current query string | Tag | Output | |---|---|---| | `?color=green` | `{% querystring %}` | `?color=green` | | `?color=green` | `{% querystring size="M" %}` | `?color=green&size=M` | | `?color=green` | `{% querystring color="red" size="S" %}` | `?color=red&size=S` | | `?color=green&size=M` | `{% querystring color=None %}` | `?size=M` | | empty | `{% querystring %}` | `?` (Django 6.0+) | ## A pagination bar ```django <nav class="pagination"> {% if page_obj.has_previous %} <a href="{% querystring page=page_obj.previous_page_number %}">Previous</a> {% endif %} <span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span> {% if page_obj.has_next %} <a href="{% querystring page=page_obj.next_page_number %}">Next</a> {% endif %} <a href="{% querystring status=None page=None %}">Clear filters</a> </nav> ``` Changing a filter should usually reset pagination as well, which is what `page=None` in the last link does. ## Requirements and edge cases - **The request must be in the context.** With no positional argument the tag reads `request.GET` from the context's request. Enable `django.template.context_processors.request` and render with the request, or pass a `QueryDict` explicitly. - **Positional arguments must be mappings** (`QueryDict`, `dict`); anything else raises `TemplateSyntaxError`, and so do non-string keys. - **Escaping.** Like other simple tags, the output passes through `conditional_escape` when autoescaping is on, so `&` becomes `&amp;` inside the attribute, which browsers decode back to `&` in the URL. - **Relative link.** The output is only a query string, so the browser applies it to the current path. To target another view, combine it with `{% url %}`: `href="{% url 'invoice-list' %}{% querystring page=1 %}"`. ## Before and after an upgrade Many projects written before 5.1 carry a hand-made helper for the same job, typically a custom `simple_tag` called something like `url_replace` that copies `request.GET`, sets one key and returns `urlencode()`. When upgrading: 1. **Search templates** for the custom tag and for loops over `request.GET`. 2. **Replace** each call with `{% querystring key=value %}`; the semantics match for single keys, and `None` now gives a clean way to drop a key. 3. **Drop hand-written question marks**: the tag's output already includes the leading `?` (always, since 6.0, even when empty), so remove any `?` the old template wrote before the custom tag's output. 4. **Delete the custom tag** once nothing loads it, so the project has one way of doing this. ## Version differences | Release | Behaviour | |---|---| | before 5.1 | no built-in tag; templates looped over `request.GET` or used a custom tag | | 5.1, 5.2 | the tag exists, but an empty result was not consistently prefixed with `?` | | 6.0+ | the result always starts with `?`, and several positional mappings are accepted | The 6.0 change matters for "clear all" links: an empty `href` does not reload the page, while `href="?"` does, which is why Django now always emits the `?`.

  • Why might {% querystring %} fail on a page rendered with render_to_string()?
    Without positional arguments the tag reads `request.GET` from the context's request. `render_to_string()` without `request=` gives it no request, so the lookup fails. Pass the request when rendering, make sure the `request` context processor is enabled, or pass a `QueryDict` to the tag explicitly.
  • How do you set a key with several values, such as tag=a&tag=b?
    Pass a list: `{% querystring tag=selected_tags %}` where `selected_tags` is `["a", "b"]` produces `tag=a&tag=b`. `None` entries in the list are dropped, and a list containing only `None` removes the key.

saying these in an interview costs you the question

  • {% querystring %} returns a full URL including the path
  • Passing an empty string removes a parameter
  • {% querystring %} discards the existing parameters and keeps only the ones you pass
  • The tag works without the request in the context
  • {% querystring %} has been built in since Django 4.x