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
| Function | Space Encoding | Best For |
|---|---|---|
quote() | %20 | Path 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:
-
URL Encoding the Space Character — + vs %20 Space Encoding Guide
-
How to URL Encode a JavaScript Object JavaScript Object Query Params
-
encodeURI vs encodeURIComponent JavaScript Encoding Guide
-
URL Encoding Explained with Real API Examples URL Encoding with Real API Examples
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.