Batu Lab NotesPractical developer guides

Normalize a percent-encoded query value

By Batu · English technical notes

Percent escapes can represent the same query value with different letter case, and form-style query values may represent spaces with +. This example normalizes the standalone value caf%c3%a9+au+lait in two stages. unquote_plus decodes percent escapes as UTF-8 and changes + to a space, producing café au lait. quote_plus then encodes that text again, producing caf%C3%A9+au+lait with uppercase escape digits and form-style spaces.

The assertions distinguish the semantic value from its normalized wire representation. Passing errors="strict" means malformed UTF-8 byte sequences raise UnicodeDecodeError instead of being silently replaced. This is a useful choice when a malformed query value should be rejected by the caller. The shown quoting behavior follows the modern RFC 3986 treatment introduced in Python 3.7; these functions themselves are available in earlier Python 3 releases.

Normalization is not validation. It does not decide whether café au lait is an allowed search term, preserve an original spelling for signatures, or parse a complete query string with repeated keys. For a full query, parse its pairs with the appropriate URL-query API and define ordering and duplicate-key policy separately.

See the official urllib.parse documentation. AI assistance disclosure: this article was drafted with AI assistance and should be reviewed for application-specific encoding rules.

from urllib.parse import quote_plus, unquote_plus


raw_value = "caf%c3%a9+au+lait"
decoded = unquote_plus(raw_value, encoding="utf-8", errors="strict")
normalized = quote_plus(decoded, encoding="utf-8", errors="strict")

assert decoded == "café au lait"
assert normalized == "caf%C3%A9+au+lait"

print(f"decoded={decoded}")
print(f"normalized={normalized}")
decoded=café au lait
normalized=caf%C3%A9+au+lait