PercentURL / Guides / Python URL encoding
Python urlencode: Put a Nested URL in One Query Parameter
Build the inner URL first, then use urllib.parse.urlencode() to add it as one value in an outer query string. Parse each layer once on the way back.
urlencode() on a mapping of outer parameter names to raw values. If one value is itself a URL, the outer encoder escapes its ?, &, =, and existing percent signs so they stay inside that single value. On receipt, parse the outer query first; do not manually decode the nested URL twice.This is useful for redirect destinations, callback URLs, and links that carry a pre-built search URL. The important boundary is the query value: your inner URL keeps its own structure, while the outer URL treats that complete inner URL as data.
Open PercentURL Encoder / Decoder
Build and encode the two layers
First encode the inner URL’s own query values. Then build the inner URL and pass it as a value to the outer urlencode() call.
from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
# Layer 1: query values that belong to the search URL.
inner_query = urlencode({
"q": "tea & cake",
"tag": "url+encode",
})
inner_url = urlunsplit((
"https", "search.example.test", "/results", inner_query, ""
))
# Layer 2: the complete inner URL is one outer parameter value.
outer_query = urlencode({
"redirect_uri": inner_url,
"source": "docs",
})
outer_url = urlunsplit((
"https", "app.example.test", "/continue", outer_query, ""
))
print(outer_url)
# Parse one layer at a time at the receiving end.
outer_values = parse_qs(urlsplit(outer_url).query)
received_inner_url = outer_values["redirect_uri"][0]
inner_values = parse_qs(urlsplit(received_inner_url).query)
print(inner_values["q"][0]) # tea & cake
print(inner_values["tag"][0]) # url+encode
The output is one outer redirect_uri=... value plus source=docs. Inside the nested value, %2526 is expected: the inner query’s %26 is percent-encoded once more while the complete inner URL is placed in the outer query. Parsing the outer query restores the inner %26; parsing the inner query then restores the ampersand in the search text.
What the encoding is protecting
| Character in the inner URL | Why it changes in the outer value | What one outer parse restores |
|---|---|---|
? | It begins the inner query, so it must not start the outer query’s structure. | The inner URL’s query boundary. |
& and = | They separate inner parameters and name/value pairs. | The inner URL’s own parameter separators. |
+ | Form-style query encoding uses + for a space; a literal plus is encoded as %2B. | The correct inner value after the outer query is parsed. |
% | It starts an existing percent escape such as %26, so the outer layer represents it as %25. | The original inner escape, ready for the inner parser. |
Append a value to an existing URL safely
If the outer URL already has parameters, split and parse its query instead of concatenating a raw &. A list of pairs preserves repeated parameter names and their order:
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
parts = urlsplit("https://app.example.test/continue?source=docs#top")
pairs = parse_qsl(parts.query, keep_blank_values=True)
pairs.append(("redirect_uri", inner_url))
updated_url = urlunsplit(parts._replace(query=urlencode(pairs)))
Use doseq=True when a mapping value is a list and the server expects one repeated query field per list item. Do not pass a nested dictionary and expect urlencode() to invent a bracket notation: choose the API’s documented representation and build its name/value pairs explicitly.
Common mistakes
| Symptom | Cause | Fix |
|---|---|---|
| The outer URL gains unexpected parameters. | The inner URL was concatenated without encoding it as one outer value. | Pass the complete inner URL as a value to the outer urlencode(). |
The inner query looks like %2526 after encoding. | The percent escape is being carried through the outer query layer. | Parse the outer query once; let the inner query parser handle its own values. |
A space appears as + rather than %20. | urlencode() uses form-style query serialization by default. | That is valid for query parameters. Use quote_via=quote only if the receiving format specifically requires percent-encoded spaces. |
| A literal plus becomes a space. | The query was decoded with the form convention even though the plus represented data. | Build query values with urlencode(); parse the query with parse_qs() or parse_qsl(). |
Validate redirect destinations separately
Encoding only preserves the parameter boundary; it does not make a destination trustworthy. If a server uses redirect_uri to redirect visitors, validate the parsed scheme and host against an allowlist before redirecting. Python’s urlsplit() separates URL components but does not validate that a URL is safe or acceptable.
For a JavaScript version of the same nested-URL task, see Online URL Encode: Put a Redirect URL in a Query Parameter. For a one-off value, use the URL Encoder Online guide or the browser tool above.
Sources
- Python documentation:
urllib.parse—urlencode(),parse_qs(),parse_qsl(),urlsplit(), andurlunsplit(). - RFC 3986 §2.2: Reserved Characters — reserved characters can delimit URI components.
- Stack Overflow: How to URL encode nested query strings? — a recurring user question about keeping an inner URL’s parameters inside one outer value; used only for problem discovery.