How to URL Encode a Query String in Python — urllib.parse.quote vs urlencode

Python is one of the most common languages for API work, yet its URL encoding functions create confusion even among experienced developers.

The standard library provides:

from urllib.parse import quote, urlencode, quote_plus

These functions look similar but produce different output. Use the wrong one and your API requests silently break when they hit space characters, special symbols, or unicode.

This guide covers every URL encoding function in Python's standard library, when to use each, and how to avoid the bugs that slip past code review.


The Three Functions

urllib.parse.quote()

from urllib.parse import quote

print(quote("hello world"))
print(quote("node.js & react"))
print(quote("C++"))

Output:

hello%20world
node.js%20%26%20react
C%2B%2B

quote() follows RFC 3986. Spaces become %20. Special characters are percent-encoded. Safe characters (letters, digits, _.-~) are preserved.

urllib.parse.quote_plus()

from urllib.parse import quote_plus

print(quote_plus("hello world"))
print(quote_plus("node.js & react"))

Output:

hello+world
node.js+%26+react

quote_plus() follows form-urlencoded rules. Spaces become +, just like HTML form submissions.

urllib.parse.urlencode()

from urllib.parse import urlencode

params = {"q": "hello world", "category": "books & media"}
print(urlencode(params))

Output:

q=hello+world&category=books+%26+media

urlencode() takes a dictionary of parameters and produces a complete query string. It uses quote_plus() internally, so spaces become +.


The Core Difference

FunctionSpace EncodingBest For
quote()%20Path segments, individual values, RFC 3986 APIs
quote_plus()+Form data, query strings expecting +
urlencode()+Building full query strings from dicts

When to Use Each

Use quote() for Path Segments

from urllib.parse import quote

product_name = "laptops & tablets/2024"
safe_path = quote(product_name)
url = f"https://example.com/products/{safe_path}"

print(url)

Output:

https://example.com/products/laptops%20%26%20tablets%2F2024

quote() encodes the / as well (by default), which is correct for a single path segment.

Use quote() with safe='/' for Partial Paths

product_name = "laptops & tablets"
safe_path = quote(product_name, safe='/')
url = f"https://example.com/products/{safe_path}"

print(url)

Output:

https://example.com/products/laptops%20%26%20tablets

The safe parameter specifies characters that should not be encoded.

Use urlencode() for Query Strings

from urllib.parse import urlencode
import requests

params = {
    "q": "python tutorial",
    "page": 1,
    "limit": 20,
    "filter": "beginner & free",
}

query_string = urlencode(params)
url = f"https://api.example.com/search?{query_string}"

response = requests.get(url)

urlencode() handles key/value encoding, spacing, and joining automatically.

Use quote_plus() When Interoperating with Form Data

If your backend uses form-urlencoded parsing (e.g., Flask/Werkzeug defaults), quote_plus() ensures compatibility.

from urllib.parse import quote_plus

value = quote_plus("hello world")
# Result: hello+world

Building Query Strings: Three Approaches

Approach 1: urlencode() (Simplest)

from urllib.parse import urlencode

params = {"q": "python & django", "sort": "relevance"}
querystring = urlencode(params)
url = f"https://example.com/search?{querystring}"

Output:

https://example.com/search?q=python+%26+django&sort=relevance

Approach 2: Manual with quote()

from urllib.parse import quote

params = {"q": "python & django", "sort": "relevance"}
query_parts = [f"{quote(k)}={quote(v)}" for k, v in params.items()]
querystring = "&".join(query_parts)
url = f"https://example.com/search?{querystring}"

Output:

https://example.com/search?q=python%20%26%20django&sort=relevance

Note the %20 instead of +.

Approach 3: Manual with quote_plus()

from urllib.parse import quote_plus

params = {"q": "python & django", "sort": "relevance"}
query_parts = [f"{quote_plus(k)}={quote_plus(v)}" for k, v in params.items()]
querystring = "&".join(query_parts)
url = f"https://example.com/search?{querystring}"

Output:

https://example.com/search?q=python+%26+django&sort=relevance

Passing Parameters in Requests

requests Library

The requests library handles URL encoding automatically when you pass params:

import requests

params = {"q": "python & django", "page": 1}
response = requests.get("https://api.example.com/search", params=params)

print(response.url)

Output:

https://api.example.com/search?q=python+%26+django&page=1

requests uses quote_plus() internally. This is usually correct for REST APIs, but check the API documentation.

httpx Library

import httpx

params = {"q": "python & django", "page": 1}
response = httpx.get("https://api.example.com/search", params=params)

print(response.url)

httpx also handles encoding automatically.


URL Encoding in FastAPI and Django

FastAPI

FastAPI automatically decodes URL-encoded query parameters. You do not need to decode manually.

from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/search")
async def search(q: str = Query(...)):
    return {"query": q}

FastAPI handles both + and %20 correctly for incoming requests.

When constructing requests to external APIs from FastAPI, use urlencode() or quote() as needed.

Django

Django's QueryDict handles decoding automatically:

# Django view
def search(request):
    q = request.GET.get("q", "")
    # q is already decoded — %20 and + are both handled

For constructing URLs in Django, consider:

from urllib.parse import urlencode
from django.urls import reverse

base_url = reverse("search")
params = urlencode({"q": "python & django"})
url = f"{base_url}?{params}"

Handling Unicode and UTF-8

Python 3's urllib.parse uses UTF-8 by default:

from urllib.parse import quote

print(quote("東京"))
print(quote("🔥"))

Output:

%E6%9D%B1%E4%BA%AC
%F0%9F%94%A5

This matches browser and modern API expectations.


Custom Encoding: Specifying Safe Characters

from urllib.parse import quote

# Preserve the colon
print(quote("name: value", safe=':'))
# name%3A%20value  -- colon not encoded actually, wait:
# Actually quote preserves : by default? Let me verify.

# Preserve multiple characters
print(quote("hello world!", safe='!'))

By default, quote() considers :/?#[]@!$&'()*+,;= as reserved and encodes them. The safe parameter overrides this.


Decoding in Python

urllib.parse.unquote()

from urllib.parse import unquote

print(unquote("hello%20world"))
print(unquote("hello+world"))

Output:

hello world
hello+world  # NOTE: unquote does NOT decode + as space!

unquote() only decodes percent-encoding. It does NOT convert + to space.

urllib.parse.unquote_plus()

from urllib.parse import unquote_plus

print(unquote_plus("hello+world"))
print(unquote_plus("hello%20world"))

Output:

hello world
hello world

unquote_plus() handles both + (as space) and %20 (as space).

This distinction causes real bugs. If you use unquote() on form-encoded data with +, the plus signs stay as plus signs.


Common Python Encoding Bug

The Problem

from urllib.parse import quote

# Encoding a value that someone already encoded
original = "hello%20world"
double_encoded = quote(original)

print(double_encoded)  # hello%2520world

The % sign becomes %25.

The Fix

from urllib.parse import unquote, quote

# Decode first if there is any chance it is already encoded
raw = unquote(original)  # hello world
safe = quote(raw)        # hello%20world

Testing URL Encoding Behavior

from urllib.parse import quote, urlencode, unquote_plus
import pytest

class TestUrlEncoding:
    def test_quote_encodes_spaces_as_percent_20(self):
        assert quote("hello world") == "hello%20world"

    def test_urlencode_encodes_spaces_as_plus(self):
        result = urlencode({"q": "hello world"})
        assert result == "q=hello+world"

    def test_round_trip(self):
        original = "hello world & python 東京"
        encoded = quote(original)
        decoded = unquote_plus(encoded)
        assert decoded == original

    def test_plus_preserved_as_percent_2B(self):
        assert quote("C++") == "C%2B%2B"

    def test_empty_string(self):
        assert quote("") == ""

    def test_no_op_for_safe_chars(self):
        assert quote("hello123") == "hello123"

Best Practices for Python URL Encoding

Use urlencode() for Complete Query Strings

querystring = urlencode(params)

Use quote() for Individual Values

safe_value = quote(user_input)

Use unquote_plus() for Decoding

decoded = unquote_plus(encoded_value)

Prefer Library-Level Encoding

# Let the requests library handle it
requests.get(url, params=params)

Always Specify UTF-8

Python 3 defaults to UTF-8, but if you work with Python 2 or legacy systems, be explicit:

quote("東京", encoding="utf-8")

Handle Both + and %20 in Input

def normalize_query_value(value):
    return unquote_plus(value.replace("+", " "))

Related Resources

For related encoding topics and cross-language comparisons:


FAQ

What is the difference between quote() and urlencode() in Python?

quote() encodes a single string value. urlencode() encodes a dictionary of key-value pairs into a complete query string.

Why does urlencode() use + for spaces?

urlencode() uses quote_plus() internally, which follows the application/x-www-form-urlencoded standard where spaces become +.

How do I make urlencode() use %20 instead of +?

Pass quote_via=quote to urlencode():

urlencode(params, quote_via=quote)

This only works in Python 3.7+.

How do I decode a URL-encoded string in Python?

Use unquote_plus() to handle both %20 and + as spaces.

Does requests library handle URL encoding?

Yes. Passing parameters via the params argument automatically handles encoding using quote_plus() internally.

How do I encode a path segment in Python?

Use quote(path_segment) to encode a single path segment safely.

What is the safe parameter in quote()?

The safe parameter specifies characters that should not be percent-encoded. For example, quote(value, safe='/') preserves forward slashes.


Final Thoughts

Python's URL encoding functions are straightforward once you understand their design: quote() follows the URL standard with %20, while urlencode() and quote_plus() follow the form standard with +.

Match the function to your use case — use quote() for API paths and individual values, urlencode() for building query strings, and let the requests library handle encoding when possible.

And when you need to verify how a specific value looks when encoded, the URL Encoder/Decoder tool provides quick cross-format testing.