Skip to main content

Preserve repeated query parameters in Python

2026-11-12 12:34:58.621055+00:00
Preserve repeated query parameters in Python

A filter URL can carry tag=python&tag=security. Turning its query into a plain dictionary too early discards one of the two values. Keep the parsed pairs until the application has decided which parameters may repeat, which must appear once, and what a blank value means.

Python's urllib.parse documentation describes parse_qsl, which parses form-style query data into a list of name and value pairs. It also documents options for blank values, malformed fields and the maximum number of fields. The example below uses those options to make a small input policy visible.

parse_qsl preserves repeated fields in a list and can reject malformed fields.

Retain both values

from urllib.parse import parse_qsl

def query_pairs(text):
    return parse_qsl(text, keep_blank_values=True,
                     strict_parsing=True, max_num_fields=100)

print(query_pairs('tag=python&tag=security&empty='))
# [('tag', 'python'), ('tag', 'security'), ('empty', '')]

The returned list retains both tag pairs and their order. The blank field is retained because keep_blank_values=True; without it, blank values are ignored by default. Decide whether empty= should mean an empty filter, an invalid filter or something else in the application. Do not silently treat it as absence unless that is the agreed contract.

strict_parsing=True makes a malformed field such as broken raise ValueError instead of being silently ignored. max_num_fields=100 bounds the number of fields accepted by this example; choose a limit suitable for your endpoint and also enforce a request-size limit before parsing. Test repeated values and a blank value as shown above, then verify that query_pairs('tag=python&broken') raises ValueError.

Validate after parsing

Strict field parsing is not a complete URL or percent-escape validator. For example, parse_qsl('tag=%ZZ', strict_parsing=True) retains %ZZ as text. Decide separately whether your protocol permits such input and reject it with an appropriate validation step when needed. The default decoding error policy can replace invalid encoded bytes, so select an error policy deliberately if preserving or rejecting such bytes matters to the application.

Group repeated values only after parsing. For a parameter that must occur once, count its occurrences and reject duplicates instead of letting a dictionary conversion choose the last one. For an allowed repeated filter, retain every value and validate each against the endpoint's rules. Unknown parameter names may need rejection as well.

This function accepts a query string, not a complete URL. If starting with a URL, split it first and pass its query component. Python's URL parsing functions do not themselves validate a URL's suitability for a security-sensitive use. Validate the scheme, host and other components according to the actual operation before making a request or redirecting a user.

Check the endpoint contract

Write an endpoint test using two different tag values and assert that the resulting filter receives both. Add a separate test for a repeated single-value parameter, such as a page number, and assert the chosen rejection. This catches accidental conversion to dict in a later refactor. If order affects the meaning of repeated values, retain and test that order through the entire request path. If order does not matter, say so in the contract rather than depending on an incidental normalization performed by another component.