urllib.parse.parse_qs
parse_qs('tag=a&tag=b') gives {'tag': ['a', 'b']} — always lists, because a name can repeat. parse_qsl keeps the raw (name, value) pairs. Both drop empty values unless keep_blank_values=True, and both expect the query without its leading '?'.
Demo
from urllib.parse import parse_qs parse_qs('tag=python&tag=web&page=2')
parse_qs splits on & only (the default separator since 3.10), so 'a=1;b=2' is one field whose value is '1;b=2'. A leading ? is not stripped either: it becomes part of the first name, '?a'. Names and values are decoded with unquote_plus, so + and %20 both become a space.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| qs | str | bytes | yes | The query string, without the leading ?. bytes give bytes names and values. |
| keep_blank_values | bool | no (False) | True keeps fields with an empty value ('q=' → ''); False drops them. |
| strict_parsing | bool | no (False) | True raises ValueError('bad query field: ...') for a field without '=' (including the empty field of a doubled or trailing separator); False skips it. |
| encoding | str | no ('utf-8') | Passed to unquote_plus for every name and value. |
| errors | str | no ('replace') | Passed to unquote_plus: invalid UTF-8 becomes U+FFFD by default. |
| max_num_fields | int | None | no (None) | Raise ValueError('Max number of fields exceeded') if the query has more fields than this (3.8+). Protects servers from huge POST bodies. |
| separator | str | no ('&') | The one string that separates fields (3.10+). Before 3.10 both & and ; were accepted. |
Return value
dict[str, list[str]] — parse_qs: every name mapped to the list of its values, in order. parse_qsl: a list of (name, value) tuples, duplicates and order kept.
Common patterns
from urllib.parse import urlsplit, parse_qs params = parse_qs(urlsplit(url).query) page = int(params.get('page', ['1'])[0])
from urllib.parse import parse_qsl params = dict(parse_qsl(query))
from urllib.parse import parse_qs form = parse_qs(body, max_num_fields=100, strict_parsing=True)
from urllib.parse import parse_qs, urlencode params = parse_qs(query) params['page'] = ['3'] new_query = urlencode(params, doseq=True)
Examples
Pitfalls
from urllib.parse import parse_qs parse_qs('page=2')['page']
from urllib.parse import parse_qs parse_qs('page=2')['page'][0]
from urllib.parse import parse_qs parse_qs('https://example.com/s?q=python')
from urllib.parse import parse_qs, urlsplit parse_qs(urlsplit('https://example.com/s?q=python').query)
from urllib.parse import parse_qs 'q' in parse_qs('q=&page=1')
from urllib.parse import parse_qs 'q' in parse_qs('q=&page=1', keep_blank_values=True)
When to use
- Reading query parameters from a URL or a form body
- Repeated names (filters, tags): parse_qs collects them
- Order or duplicates matter: parse_qsl
- Building a query string → urlencode
- JSON request bodies → json.loads
- Inside a web framework → its request.args / request.GET already did this
Notes
FAQ
parse_qs(urlsplit(url).query) returns a dict of lists, e.g. {'q': ['python'], 'page': ['2']}. Use parse_qsl for (name, value) pairs, or dict(parse_qsl(...)) for one value per name (the last one wins).