X Xerobit

URL Encoding in Python — urllib.parse, requests, and FastAPI

Python's urllib.parse module provides quote, quote_plus, and urlencode for URL encoding. Here's how to encode URLs and query strings correctly in Python, requests library, and...

Mian Ali Khalid · · 4 min read
Use the tool
URL Encoder / Decoder
Percent-encode and decode URLs per RFC 3986.
Open URL Encoder / Decoder →

Python’s urllib.parse module has everything needed for URL encoding. The key decision: use quote for path segments, quote_plus for query string values, and urlencode for multiple query parameters.

Use the URL Encoder to encode and decode URLs online.

urllib.parse functions

from urllib.parse import quote, quote_plus, urlencode, urljoin, urlparse

# quote: percent-encodes special characters (for paths)
quote('hello world')          # 'hello%20world'
quote('fish & chips')         # 'fish%20%26%20chips'
quote('/path/to/resource')    # '/path/to/resource' (/ is safe by default)

# Encode / too:
quote('/path/to/file', safe='')  # '%2Fpath%2Fto%2Ffile'

# quote_plus: encodes spaces as + (for query strings)
quote_plus('hello world')     # 'hello+world'
quote_plus('fish & chips')    # 'fish+%26+chips'
quote_plus('price=10')        # 'price%3D10'

# urlencode: encode multiple query params as key=value pairs
params = {'q': 'fish & chips', 'format': 'json', 'page': 1}
urlencode(params)             # 'q=fish+%26+chips&format=json&page=1'

# With doseq=True for lists:
params = {'tags': ['python', 'api', 'web']}
urlencode(params, doseq=True)  # 'tags=python&tags=api&tags=web'

Building URLs correctly

from urllib.parse import urlencode, urljoin

# Build a URL with query parameters:
base = 'https://api.example.com/search'
params = {
    'q': 'hello & world',
    'format': 'json',
    'page': 1,
    'sort': 'date:desc',
}

url = f'{base}?{urlencode(params)}'
# 'https://api.example.com/search?q=hello+%26+world&format=json&page=1&sort=date%3Adesc'

# urljoin for relative URL resolution:
urljoin('https://example.com/docs/', 'api-reference')
# 'https://example.com/docs/api-reference'

urljoin('https://example.com/docs/intro', '../guide')
# 'https://example.com/docs/guide'

Decoding URLs

from urllib.parse import unquote, unquote_plus, parse_qs, parse_qsl

# Decode percent-encoded strings:
unquote('hello%20world')            # 'hello world'
unquote('fish%20%26%20chips')       # 'fish & chips'

# Decode + as space (query string style):
unquote_plus('hello+world')         # 'hello world'
unquote_plus('hello%20world')       # 'hello world' (works for both)

# Parse query string:
parse_qs('q=hello+world&format=json&tags=a&tags=b')
# {'q': ['hello world'], 'format': ['json'], 'tags': ['a', 'b']}

# parse_qsl preserves order and allows duplicates:
parse_qsl('a=1&b=2&a=3')
# [('a', '1'), ('b', '2'), ('a', '3')]

With the requests library

requests handles URL encoding automatically:

import requests

# Pass params as dict — requests encodes automatically:
response = requests.get(
    'https://api.example.com/search',
    params={
        'q': 'fish & chips',
        'format': 'json',
        'page': 1,
    }
)
# Actual URL: https://api.example.com/search?q=fish+%26+chips&format=json&page=1

# Inspect the prepared URL:
print(response.request.url)

# For POST with form-encoded body:
response = requests.post(
    'https://example.com/login',
    data={'username': 'alice', 'password': 'p@ssw0rd!'},
    # Content-Type: application/x-www-form-urlencoded
)

# For POST with JSON body (not URL encoded):
response = requests.post(
    'https://api.example.com/users',
    json={'name': 'Alice', 'email': 'alice@example.com'},
    # Content-Type: application/json
)

FastAPI URL parameters

FastAPI handles encoding/decoding automatically:

from fastapi import FastAPI, Query
from typing import Optional

app = FastAPI()

# Path parameters (decoded automatically):
@app.get('/items/{item_id}')
async def get_item(item_id: str):
    return {'id': item_id}
# GET /items/hello%20world → item_id = "hello world"

# Query parameters (decoded automatically):
@app.get('/search')
async def search(
    q: str = Query(...),
    format: str = Query(default='json'),
    page: int = Query(default=1, ge=1),
):
    return {'q': q, 'format': format, 'page': page}
# GET /search?q=hello+world&page=2 → q = "hello world"

Encoding URL components manually

from urllib.parse import urlparse, urlunparse, urlencode

def build_url(scheme, host, path, params=None, fragment=None):
    """Build a properly encoded URL."""
    query = urlencode(params) if params else ''
    return urlunparse((scheme, host, path, '', query, fragment or ''))

build_url(
    'https',
    'api.example.com',
    '/search',
    params={'q': 'hello & world', 'page': 1}
)
# 'https://api.example.com/search?q=hello+%26+world&page=1'

Common mistakes

# WRONG: formatting directly into URL
user_input = "search term & more"
url = f"https://api.com/search?q={user_input}"
# 'https://api.com/search?q=search term & more'  ← spaces and & not encoded!

# CORRECT: use urlencode or quote_plus
url = f"https://api.com/search?{urlencode({'q': user_input})}"
# 'https://api.com/search?q=search+term+%26+more'

# WRONG: using quote for query string values
url = f"https://api.com/search?q={quote(user_input)}"
# 'https://api.com/search?q=search%20term%20%26%20more'  ← works but space should be +

# CORRECT for query values:
url = f"https://api.com/search?q={quote_plus(user_input)}"

Related posts

Related tool

URL Encoder / Decoder

Percent-encode and decode URLs per RFC 3986.

Written by Mian Ali Khalid. Part of the Dev Productivity pillar.