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 '?'.

urllib.parse functionPython 3.0+Live demo
Common call
parse_qs(urlsplit(url).query)
Returns
{'q': ['python'], 'page': ['2']}
Replaces
query.split('&') and split('=') by hand
Watch out
Values are lists — use ['page'][0]; and 'q=' disappears without keep_blank_values=True
urllib.parse.parse_qs(qsqs — The query string, without the leading ?. bytes give bytes names and values.type: str | bytes · required, keep_blank_valueskeep_blank_values — True keeps fields with an empty value ('q=' → ''); False drops them.type: bool · default: False=False, strict_parsingstrict_parsing — True raises ValueError('bad query field: ...') for a field without '=' (including the empty field of a doubled or trailing separator); False skips it.type: bool · default: False=False, encodingencoding — Passed to unquote_plus for every name and value.type: str · default: 'utf-8'='utf-8', errorserrors — Passed to unquote_plus: invalid UTF-8 becomes U+FFFD by default.type: str · default: 'replace'='replace', max_num_fieldsmax_num_fields — Raise ValueError('Max number of fields exceeded') if the query has more fields than this (3.8+). Protects servers from huge POST bodies.type: int | None · default: None=None, separatorseparator — The one string that separates fields (3.10+). Before 3.10 both & and ; were accepted.type: str · default: '&'='&')
→ dict[str, list[str]]

Demo

Live evaluation
A dict of lists. Repeated names collect every value.
Try:
Inputs
querystra query string, no ?
Code
from urllib.parse import parse_qs
parse_qs('tag=python&tag=web&page=2')
Result
{'tag': ['python', '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

NameTypeRequiredDescription
qsstr | bytesyesThe query string, without the leading ?. bytes give bytes names and values.
keep_blank_valuesboolno (False)True keeps fields with an empty value ('q=' → ''); False drops them.
strict_parsingboolno (False)True raises ValueError('bad query field: ...') for a field without '=' (including the empty field of a doubled or trailing separator); False skips it.
encodingstrno ('utf-8')Passed to unquote_plus for every name and value.
errorsstrno ('replace')Passed to unquote_plus: invalid UTF-8 becomes U+FFFD by default.
max_num_fieldsint | Noneno (None)Raise ValueError('Max number of fields exceeded') if the query has more fields than this (3.8+). Protects servers from huge POST bodies.
separatorstrno ('&')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

Query parameters of a URL
Split the URL first; parse only its query.
from urllib.parse import urlsplit, parse_qs
params = parse_qs(urlsplit(url).query)
page = int(params.get('page', ['1'])[0])
One value per name
dict over parse_qsl keeps the LAST value of a repeated name.
from urllib.parse import parse_qsl
params = dict(parse_qsl(query))
Limit untrusted input
Reject absurdly long bodies before building the dict.
from urllib.parse import parse_qs
form = parse_qs(body, max_num_fields=100, strict_parsing=True)
Round trip with urlencode
parse_qs output goes straight back into urlencode with doseq=True.
from urllib.parse import parse_qs, urlencode
params = parse_qs(query)
params['page'] = ['3']
new_query = urlencode(params, doseq=True)

Examples

1. Repeated names → lists
from urllib.parse import parse_qs parse_qs('tag=a&tag=b&page=2')
Returns
{'tag': ['a', 'b'], 'page': ['2']}
2. parse_qsl keeps pairs
from urllib.parse import parse_qsl parse_qsl('tag=a&tag=b&page=2')
Returns
[('tag', 'a'), ('tag', 'b'), ('page', '2')]
3. From a full URL
from urllib.parse import parse_qs, urlparse parse_qs(urlparse('https://example.com/s?q=python&page=2').query)
Returns
{'q': ['python'], 'page': ['2']}
4. Escapes and + are decoded
from urllib.parse import parse_qs parse_qs('name=Ada+Lovelace&city=Z%C3%BCrich')
Returns
{'name': ['Ada Lovelace'], 'city': ['Zürich']}
5. Blank values: dropped or kept
from urllib.parse import parse_qs (parse_qs('q=&page=1'), parse_qs('q=&page=1', keep_blank_values=True))
Returns
({'page': ['1']}, {'q': [''], 'page': ['1']})
6. A custom separator
from urllib.parse import parse_qsl parse_qsl('a=1;b=2', separator=';')
Returns
[('a', '1'), ('b', '2')]
7. max_num_fields
from urllib.parse import parse_qsl parse_qsl('a=1&b=2&c=3', max_num_fields=2)
Returns
ValueError: Max number of fields exceeded
8. bytes in, bytes out
from urllib.parse import parse_qsl parse_qsl(b'a=1&b=%FF')
Returns
[(b'a', b'1'), (b'b', b'\xff')]

Pitfalls

1. Forgetting that values are lists
parse_qs always returns lists, even for a name that appears once. Take [0] (or use parse_qsl) when you want one value.
['page']
from urllib.parse import parse_qs
parse_qs('page=2')['page']
['2']
['page'][0]
from urllib.parse import parse_qs
parse_qs('page=2')['page'][0]
'2'
2. Parsing the whole URL
parse_qs does not look for the ?. Given a full URL, the scheme, host and path end up inside the first name.
full URL
from urllib.parse import parse_qs
parse_qs('https://example.com/s?q=python')
{'https://example.com/s?q': ['python']}
the query
from urllib.parse import parse_qs, urlsplit
parse_qs(urlsplit('https://example.com/s?q=python').query)
{'q': ['python']}
3. Empty parameters vanish
A search box submitted empty sends 'q='. By default it is dropped, so 'q' in params is False.
default
from urllib.parse import parse_qs
'q' in parse_qs('q=&page=1')
False
keep_blank_values=True
from urllib.parse import parse_qs
'q' in parse_qs('q=&page=1', keep_blank_values=True)
True

When to use

Use it
  • Reading query parameters from a URL or a form body
  • Repeated names (filters, tags): parse_qs collects them
  • Order or duplicates matter: parse_qsl
Reach for something else
  • Building a query string → urlencode
  • JSON request bodies → json.loads
  • Inside a web framework → its request.args / request.GET already did this

Notes

CPython impl
parse_qs is a loop over parse_qsl that appends each value to a list per name; parse_qsl splits on separator, partitions each field at the first =, and decodes both halves with unquote_plus
Separator
Since 3.10 only one separator is used ('&' by default); 'a=1;b=2' is a single field
Exceptions
ValueError for strict_parsing failures ('bad query field: ...'), max_num_fields ('Max number of fields exceeded') and an empty or non-string separator

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).