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.

Quick answer: Use 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 URLWhy it changes in the outer valueWhat 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

SymptomCauseFix
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