URL encoding in Python
Python ships with everything you need for URL encoding in the standard library — no third-party packages required. The functions live in urllib.parse, and once you know which to use when, URL encoding becomes a two-line problem in Python.
This article covers every function, when to use each, and the gotchas that trip people up.
The five functions you actually need
Out of the dozen URL-related functions in urllib.parse, five do the real work:
from urllib.parse import quote, quote_plus, unquote, unquote_plus, urlencode
quote — RFC 3986 percent-encoding. Space becomes %20. The default for new code.
quote_plus — Form-encoded variant. Space becomes +. Use for query string values when the receiver expects form encoding (most browsers send this for GET form submissions).
unquote — Reverse of quote. Decodes %XX sequences. Does NOT treat + as space.
unquote_plus — Reverse of quote_plus. Decodes %XX AND turns + back into space.
urlencode — Takes a dict (or list of tuples) and builds a complete query string with form encoding.
quote and unquote: the basics
>>> from urllib.parse import quote, unquote
>>> quote('Hello, World!')
'Hello%2C%20World%21'
>>> unquote('Hello%2C%20World%21')
'Hello, World!'
By default, quote leaves forward slash (/) unencoded so you can call it on a path safely:
>>> quote('/api/My Item/details')
'/api/My%20Item/details'
If you need to encode slashes too (because your data legitimately contains them), pass safe='':
>>> quote('/api/My Item/details', safe='')
'%2Fapi%2FMy%20Item%2Fdetails'
The opposite — leaving additional characters unencoded — uses the same parameter:
>>> quote('hello world!', safe='!')
'hello%20world!'
quote_plus: when the receiver expects forms
Most server-side frameworks (Django, Flask, FastAPI, Express, Rails, anything that handles HTML form submissions) accept both + and %20 as space in query strings. But some old APIs only accept +. Match what the receiver expects:
>>> quote_plus('Hello, World!')
'Hello%2C+World%21'
Notice: comma still becomes %2C, but space is now +. That’s the only difference between quote and quote_plus.
urlencode: building query strings from dicts
This is what you reach for 80% of the time. Give it a dict, get a query string:
>>> from urllib.parse import urlencode
>>> urlencode({'q': 'Hello, World!', 'lang': 'en'})
'q=Hello%2C+World%21&lang=en'
If you have repeated keys (an array of values), use a list of tuples instead of a dict:
>>> urlencode([('tag', 'foo'), ('tag', 'bar'), ('tag', 'baz')])
'tag=foo&tag=bar&tag=baz'
Or use the doseq=True flag to expand list values automatically:
>>> urlencode({'tag': ['foo', 'bar', 'baz']}, doseq=True)
'tag=foo&tag=bar&tag=baz'
Working with full URLs: urlparse and urlunparse
When you have a complete URL and need to dissect or rebuild it:
>>> from urllib.parse import urlparse
>>> u = urlparse('https://example.com/search?q=test&page=2#section')
>>> u.scheme
'https'
>>> u.netloc
'example.com'
>>> u.path
'/search'
>>> u.query
'q=test&page=2'
>>> u.fragment
'section'
And to parse the query into a dict:
>>> from urllib.parse import parse_qs, parse_qsl
>>> parse_qs('q=Hello%2C+World%21&lang=en')
{'q': ['Hello, World!'], 'lang': ['en']}
>>> parse_qsl('q=hello&lang=en')
[('q', 'hello'), ('lang', 'en')]
parse_qs returns a dict where every value is a list (because keys can repeat). parse_qsl returns a list of tuples, preserving order.
Real-world patterns
Building a search URL
from urllib.parse import urlencode
base = 'https://api.example.com/search'
params = {
'q': user_query,
'lang': 'en',
'page': 2,
'limit': 50,
}
url = f'{base}?{urlencode(params)}'
Adding a parameter to an existing URL
from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse
def add_param(url, key, value):
parsed = urlparse(url)
params = parse_qsl(parsed.query)
params.append((key, value))
new_query = urlencode(params)
return urlunparse(parsed._replace(query=new_query))
>>> add_param('https://example.com/page?ref=ad', 'utm_source', 'twitter')
'https://example.com/page?ref=ad&utm_source=twitter'
URL-encoding inside f-strings
from urllib.parse import quote
user_input = "café"
url = f"https://api.example.com/search?q={quote(user_input)}"
# 'https://api.example.com/search?q=caf%C3%A9'
Bytes vs strings (and why it matters)
Python 3 distinguishes bytes from strings. The encoding functions accept either:
>>> quote('café') # str input → assumes UTF-8
'caf%C3%A9'
>>> quote(b'\xc3\xa9') # bytes input → percent-encodes each byte
'%C3%A9'
>>> quote('café', encoding='latin-1') # force a different charset
'caf%E9'
For non-UTF-8 sources (legacy systems), pass the explicit encoding parameter.
The requests library handles most of this for you
If you're using the third-party requests library, params encoding is automatic:
import requests
response = requests.get('https://api.example.com/search', params={
'q': 'Hello, World!',
'lang': 'en',
})
# requests builds the URL: https://api.example.com/search?q=Hello%2C+World%21&lang=en
requests uses urllib.parse.urlencode under the hood, so the output is identical to building the URL manually.
Common Python gotchas
quote leaves / unencoded by default
This is convenient for paths but wrong for path segment values. If your value might contain a slash, pass safe=''.
>>> quote('A/B Testing') # wrong for path segment
'A/B%20Testing'
>>> quote('A/B Testing', safe='') # right
'A%2FB%20Testing'
The urlencode dict ordering changed in Python 3.7
Before 3.7, dict order wasn’t guaranteed, so the query string order was nondeterministic. From 3.7 onward, dicts preserve insertion order and urlencode follows it. If you need a specific order on older Python, pass a list of tuples instead.
Don’t roll your own with %-encoding
I’ve seen code like:
url = base + '?q=' + user_input.replace(' ', '%20') # wrong
This handles spaces but breaks on every other special character (&, ?, =, #). Always use the standard library.
Quick reference card
| Goal | Function |
|---|---|
| Encode a single value (RFC 3986) | quote(value) |
| Encode for query string (form style) | quote_plus(value) |
| Build whole query from dict | urlencode(dict) |
| Decode (no + → space) | unquote(value) |
| Decode (with + → space) | unquote_plus(value) |
| Parse a URL | urlparse(url) |
| Parse query string to dict | parse_qs(query) |
That’s the entire Python URL encoding toolkit. Reach for urlencode when building, parse_qs when consuming, and quote / unquote when you need single-value control.
Found this useful? Try the URL decoder, the URL encoder, or browse all tools.