Article · Apr 28, 2026 · 7 min read

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 dicturlencode(dict)
Decode (no + → space)unquote(value)
Decode (with + → space)unquote_plus(value)
Parse a URLurlparse(url)
Parse query string to dictparse_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.

Frequently asked

Common questions.

Use quote() for path segments — it encodes a space as %20. Use quote_plus() for query-string values that a form-style receiver expects, where a space becomes +. For building a whole query string from a dictionary, urlencode() applies the form convention for you.

By design. urllib.parse.quote() has a safe parameter that defaults to '/', so slashes pass through unencoded — convenient for whole paths. If you’re encoding a single segment that may contain a literal slash, pass safe='' so the slash is encoded as %2F.

Use urllib.parse.urlencode(params). It takes a dict of key-value pairs and returns a fully encoded query string, applying the application/x-www-form-urlencoded convention (spaces as +). Since Python 3.7 the output preserves insertion order of the dict.

The requests library encodes query parameters for you when you pass a params= dict, so you rarely call quote() directly for HTTP calls. Reach for urllib.parse when you’re constructing or parsing URLs yourself rather than making a request.

Sources & standards

Honest scope: the Python docs are authoritative for function behaviour; RFC 3986 defines the encoding the functions implement.

More reading

From the blog.

About this page

Written and maintained by the urlencodedecode.com team. Every technical claim on this page is verified against primary sources — the RFCs (3986, 3629, 4648, 7578), the WHATWG URL Standard, and official vendor or language documentation — rather than second-hand summaries. When a source contradicts a common assumption, we follow the source and note the discrepancy. Corrections: contactus@urlencodedecode.com.