URL encode in Python

urllib.parse.quote(value, safe="")

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="") gives a%2520b, because the % is escaped again.
  • Bytes are quoted as they are, and encoding= picks another charset for a str: quote("café", encoding="latin-1", safe="") is caf%E9, which a UTF-8 server can’t read.

Try it in the URL Encoder & Decoder

As one value in a URL

Escaped the way encodeURIComponent does: the marks that mean something in a URL, like &, = and /, become escapes too.

As a whole URL

Escaped the way encodeURI does: the marks that shape the URL itself, like ://, ? and &, stay readable.

Runs in your browser. Nothing you enter is sent anywhere.