urllib.parse.quote(value, safe="") percent-encodes a string so it can sit inside one part of a URL. It encodes the text as UTF-8 and escapes every byte except ASCII letters, digits and _ . - ~, the unreserved characters of RFC 3986.
from urllib.parse import quote
value = "AC/DC & friends"
print(quote(value, safe="")) # AC%2FDC%20%26%20friends
print(quote(value)) # AC/DC%20%26%20friends
print(quote("café ☕", safe="")) # caf%C3%A9%20%E2%98%95
The tool below shows the first result for the same text, under As one value in a URL.
The default safe=“/” trap
quote leaves / unescaped unless told otherwise, because it was written for paths. That’s right for a whole path, quote("/files/Q3 report.pdf") gives /files/Q3%20report.pdf, and wrong for a value: the / in AC/DC would split one path segment into two. Pass safe="" whenever you encode a single value.
Query strings: quote_plus and urlencode
from urllib.parse import quote, quote_plus, urlencode
quote_plus("AC/DC & friends") # 'AC%2FDC+%26+friends'
urlencode({"q": "café & cake", "page": 2}) # 'q=caf%C3%A9+%26+cake&page=2'
urlencode({"tag": ["a", "b"]}, doseq=True) # 'tag=a&tag=b'
urlencode({"tag": ["a", "b"]}) # 'tag=%5B%27a%27%2C+%27b%27%5D'
urlencode({"q": "café & cake"}, quote_via=quote) # 'q=caf%C3%A9%20%26%20cake'
quote_plus is the HTML form encoding, application/x-www-form-urlencoded: a space becomes +, and its safe is already empty. urlencode builds a whole query string from a dict or a list of pairs, running quote_plus over every key and value. Without doseq=True a list is turned into its repr and then encoded, which is almost never what you want. quote_via=quote writes %20 instead of +, and a None value becomes the text None. Requests does this for you: params={"q": "AC/DC & friends", "tag": ["a", "b"]} gives ?q=AC%2FDC+%26+friends&tag=a&tag=b.
Python and JavaScript differ on ! ’ ( ) *
quote("it's (fun)! *now*", safe="") # 'it%27s%20%28fun%29%21%20%2Anow%2A'
JavaScript’s encodeURIComponent leaves those five marks as they are, and the tool below follows it. Both forms decode to the same text, but Python’s is the one that signing schemes such as AWS Signature Version 4 expect. Python has left ~ unescaped since 3.7.
Other gotchas
- Encode once:
quote("a%20b", safe="")givesa%2520b, because the%is escaped again. - Bytes are quoted as they are, and
encoding=picks another charset for astr:quote("café", encoding="latin-1", safe="")iscaf%E9, which a UTF-8 server can’t read.