urllib.parse.urlencode

The way to build a query string. Values that are not str or bytes are passed through str() — so a list becomes "['a', 'b']" unless you pass doseq=True. Spaces become + (quote_plus); pass quote_via=quote for %20.

urllib.parse functionPython 3.0+Live demo
Common call
urlencode({'q': 'rock & roll', 'page': 2})
Returns
'q=rock+%26+roll&page=2'
Replaces
'&'.join(f'{k}={v}' ...) — which breaks on & = and spaces
Watch out
List values need doseq=True, or they are encoded as their repr
urllib.parse.urlencode(queryquery — A dict (anything with .items()) or a list of (key, value) tuples — the list form allows repeated keys in any order. A str or a list of lists raises TypeError.type: Mapping | sequence of pairs · required, doseqdoseq — True: a sequence value produces one key=item pair per item. False: every value goes through str().type: bool · default: False=False, safesafe — Characters quote_via must not encode, e.g. "/" or ":".type: str · default: ''='', encodingencoding — Passed to quote_via for str keys and values (None means UTF-8).type: str · default: None=None, errorserrors — Passed to quote_via for str keys and values.type: str · default: None=None, quote_viaquote_via — The quoting function (3.5+). quote gives %20 for spaces instead of +.type: callable · default: quote_plus=quote_plus)
→ str

Demo

Live evaluation
A search query and a page number, encoded as a query string.
Try:
Inputs
qstrsearch text
pageintpage number
Code
from urllib.parse import urlencode
urlencode({'q': 'rock & roll', 'page': 2})
Result
'q=rock+%26+roll&page=2'

Every key and value is quoted with quote_plus and safe='': a space becomes +, and & = + / ? are all percent-encoded, so they cannot break the query apart. With doseq=False a list is first turned into its str(), "['python', 'web']", and that text is encoded; doseq=True repeats the key once per item — and an empty list then produces nothing at all.

Parameters

NameTypeRequiredDescription
queryMapping | sequence of pairsyesA dict (anything with .items()) or a list of (key, value) tuples — the list form allows repeated keys in any order. A str or a list of lists raises TypeError.
doseqboolno (False)True: a sequence value produces one key=item pair per item. False: every value goes through str().
safestrno ('')Characters quote_via must not encode, e.g. "/" or ":".
encodingstrno (None)Passed to quote_via for str keys and values (None means UTF-8).
errorsstrno (None)Passed to quote_via for str keys and values.
quote_viacallableno (quote_plus)The quoting function (3.5+). quote gives %20 for spaces instead of +.

Return value

str — 'key=value&key2=value2' with every key and value percent-encoded. No leading '?'.

Common patterns

Full URL with a query
urlencode returns the part after ?.
from urllib.parse import urlencode
url = 'https://api.example.com/search?' + urlencode({'q': term, 'limit': 50})
Repeated keys
Either a list of pairs, or a dict of lists with doseq=True.
from urllib.parse import urlencode
urlencode([('tag', 'a'), ('tag', 'b')])
urlencode({'tag': ['a', 'b']}, doseq=True)
Skip None values
None is encoded as the text None; filter first.
from urllib.parse import urlencode
query = urlencode({k: v for k, v in params.items() if v is not None})
Merge into an existing URL
parse_qs the old query, update, urlencode with doseq=True.
from urllib.parse import urlsplit, parse_qs, urlencode
parts = urlsplit(url)
params = parse_qs(parts.query)
params['page'] = [str(page)]
url = parts._replace(query=urlencode(params, doseq=True)).geturl()

Examples

1. A dict
from urllib.parse import urlencode urlencode({'q': 'rock & roll', 'page': 2})
Returns
'q=rock+%26+roll&page=2'
2. %20 instead of +
from urllib.parse import urlencode, quote urlencode({'q': 'rock & roll'}, quote_via=quote)
Returns
'q=rock%20%26%20roll'
3. Lists with doseq=True
from urllib.parse import urlencode urlencode({'tag': ['a', 'b']}, doseq=True)
Returns
'tag=a&tag=b'
4. A list of pairs
from urllib.parse import urlencode urlencode([('a', 1), ('a', 2), ('b', 'x')])
Returns
'a=1&a=2&b=x'
5. Values go through str()
from urllib.parse import urlencode urlencode({'flag': True, 'none': None, 'price': 9.5})
Returns
'flag=True&none=None&price=9.5'
6. safe= keeps characters
from urllib.parse import urlencode urlencode({'path': '/a/b'}, safe='/')
Returns
'path=/a/b'
7. Round trip with parse_qs
from urllib.parse import urlencode, parse_qs parse_qs(urlencode({'tag': ['a', 'b'], 'q': 'x y'}, doseq=True))
Returns
{'tag': ['a', 'b'], 'q': ['x y']}
8. A string is not a query
from urllib.parse import urlencode urlencode('a=1')
Returns
TypeError: not a valid non-string sequence or mapping object

Pitfalls

1. A list value without doseq
The list is converted with str() and encoded as one value — the server receives the text ['a', 'b'].
doseq=False
from urllib.parse import urlencode
urlencode({'tag': ['a', 'b']})
'tag=%5B%27a%27%2C+%27b%27%5D'
doseq=True
from urllib.parse import urlencode
urlencode({'tag': ['a', 'b']}, doseq=True)
'tag=a&tag=b'
2. Pairs as lists instead of tuples
urlencode checks that the first item of a sequence is a tuple. A list of lists (e.g. from JSON) is rejected.
list of lists
from urllib.parse import urlencode
urlencode([['a', 1], ['b', 2]])
TypeError: not a valid non-string sequence or mapping object
list of tuples
from urllib.parse import urlencode
urlencode([tuple(p) for p in [['a', 1], ['b', 2]]])
'a=1&b=2'
3. None becomes the text None
Optional parameters set to None are sent as the string None. Leave them out instead.
None kept
from urllib.parse import urlencode
urlencode({'q': 'x', 'lang': None})
'q=x&lang=None'
filter first
from urllib.parse import urlencode
params = {'q': 'x', 'lang': None}
urlencode({k: v for k, v in params.items() if v is not None})
'q=x'

When to use

Use it
  • Building any query string or form-encoded POST body from data
  • Values containing & = + / spaces or non-ASCII text
Reach for something else
  • Encoding one value → quote / quote_plus
  • JSON APIs → send json.dumps(...) as the body instead
  • requests / httpx → pass params= and they call urlencode for you

Notes

CPython impl
A mapping is iterated with .items(); keys and values that are bytes are quoted directly, everything else goes through str() and quote_via(value, safe, encoding, errors)
Sequences
With doseq=True, any value with a len() (list, tuple, but not str or bytes) is iterated; other values are str()-ed
Encoding
Form encoding (application/x-www-form-urlencoded): spaces as +, everything except letters, digits and _ . - ~ escaped

FAQ

urllib.parse.urlencode({'q': 'x y', 'page': 2}) returns 'q=x+y&page=2'. Add '?' yourself when appending it to a URL.