DevTools Hub

Search tools

Search for a developer tool

URL Encoding in Python

Part of the Encoding Toolkit

Python's urllib.parse splits encoding across several functions rather than one, and two of the defaults are easy to get wrong in ways that don't error — they just quietly produce the wrong string. The general encoding rules are unchanged from URL Encoding Explained; this is about where Python's API surface differs from what encodeURIComponent does in one call.

quote() leaves / unescaped by default

quote() takes a safe argument — characters to leave alone even though they'd normally be escaped — and it defaults to safe="/", not an empty string:

from urllib.parse import quote

quote("a/b/c")
# 'a/b/c'          — the / is left alone by default

quote("a/b/c", safe="")
# 'a%2Fb%2Fc'       — escaped, once you override the default

This is the opposite default from JavaScript's encodeURIComponent, which always escapes /. It makes sense for encoding a whole path at once (where / is the segment separator you want preserved), but it's a real bug if you use plain quote() on a single path segment that happens to contain a literal / — pass safe="" explicitly when encoding one value, not a whole path.

quote() vs. quote_plus(): the space question again

Same split as covered in URL Encoding in Node.js for Node's two APIs — Python has the equivalent split, just as two separate functions instead of two modules:

quote("hello world")       # 'hello%20world'
quote_plus("hello world")  # 'hello+world'

quote_plus is the one built for query strings — it follows the same application/x-www-form-urlencoded convention discussed in URL Encoding Explained, and it's what urlencode (below) uses internally.

The decode-side mismatch this creates

The two encoders have matching decoders — unquote for quote, unquote_plus for quote_plus — and using the wrong one on the way back out doesn't error, it just leaves a stray + in the result instead of turning it into a space:

from urllib.parse import unquote, unquote_plus

unquote("hello+world")       # 'hello+world'  — unchanged, + is not special to unquote
unquote_plus("hello+world")  # 'hello world'   — decoded correctly

If a value was encoded with quote_plus (or built via urlencode) but decoded with plain unquote, every encoded space silently survives as a literal + character in the output. Match the pair you used to encode.

urlencode() and the doseq trap

urlencode builds a full query string from a dict, using quote_plus under the hood. For a value that's a list — meant to become a repeated key — it needs an explicit flag, or it encodes the list's Python string representation instead of repeating the key:

from urllib.parse import urlencode

urlencode({"tag": ["js", "css"]})
# 'tag=%5B%27js%27%2C+%27css%27%5D'   — wrong: that's "['js', 'css']", encoded as one string

urlencode({"tag": ["js", "css"]}, doseq=True)
# 'tag=js&tag=css'                     — right: doseq=True expands the list into repeated keys

Forgetting doseq=True doesn't raise anything — the query string comes out syntactically valid, just carrying a nonsense value for that key.

parse_qs groups repeated keys automatically

On the parsing side, both parse_qs and parse_qsl handle repeated keys without any extra flag — the same behavior Node's querystring.parse has, and the opposite of what URLSearchParams.get() does in JavaScript (see How Query Parameters Work):

from urllib.parse import parse_qs, parse_qsl

parse_qs("tag=js&tag=css")
# {'tag': ['js', 'css']}

parse_qsl("tag=js&tag=css")
# [('tag', 'js'), ('tag', 'css')]

One character-set difference worth knowing

Python's quote() escapes ! * ' ( ) by default; JavaScript's encodeURIComponent leaves that exact set unescaped (covered in What Characters Need URL Encoding?). Both outputs are valid and decode to the same value — but if you're diffing an encoded string produced by a Python service against one produced by a JS client, don't expect them to match byte-for-byte even for identical input.

Try it yourself

Check what a value encodes to with URL Encode and URL Decode, or build a full query string — including repeated keys — with Query Parameter Builder. All run entirely in your browser.

Related tools