URL and percent hexadecimal encoding represent selected bytes using a percent sign followed by two hexadecimal digits. A space, for example, becomes %20. This lets a URL carry data that might otherwise be confused with its punctuation or cannot appear directly in a particular component.
The tricky part is deciding what counts as data. A slash can separate folders in a path, but an ampersand inside a search phrase should not create another query parameter. Encoding everything indiscriminately can damage a link just as easily as leaving everything unchanged.
A reliable approach starts with the URL component you are building. Encode a query value as a value, preserve separators that belong to the URL's structure, and use the matching parser when reading it back. The examples below show how that works for spaces, literal plus signs, international text, and URLs accidentally encoded twice.
The Percent-Escape Rule
Percent-encoding represents one byte as %HH, where HH is that byte's two-digit hexadecimal value. Characters outside ASCII normally become UTF-8 bytes first, so one character may need several escapes. Which bytes should be escaped depends on the URL component and the encoding convention being used.
Separate the URL's Structure from Its Data
Consider the relative URL /catalog?q=tea&sort=price#details. It has several parts with different roles:
/catalogis the path.?starts the query string.q=teaandsort=priceare query parameters under the common form-style convention.&separates those parameters.#detailsidentifies the fragment.
Now change the search phrase to tea & biscuits. Simply inserting it after q= leaves a literal ampersand where the query parser expects a separator. The application may receive a different set of fields than intended.
Encode the phrase as a single parameter value instead. A component-style result is tea%20%26%20biscuits. A form-style serializer may use tea+%26+biscuits. Both protect the ampersand when parsed using the intended convention.
This is why URL encoding should happen while assembling components. Once structural delimiters and user data have been mixed into one string, the encoder cannot reliably infer which punctuation you intended to preserve.

Read the Hexadecimal Bytes Behind Common Escapes
Each percent escape contains exactly two hexadecimal digits after %. The digits describe a byte value, not a decimal number and not necessarily an entire character.
Data Character | Hexadecimal Byte | Percent Escape |
Space |
|
|
Percent sign |
|
|
Ampersand |
|
|
Plus sign |
|
|
Slash |
|
|
Equals sign |
|
|
Question mark |
|
|
Hash sign |
|
|
These examples show how to represent characters when they need escaping as data. They do not mean every occurrence must be replaced throughout every URL.
For instance, the slash in /catalog/items separates path segments. Replacing it with %2F can change routing or be rejected by the server. In contrast, a slash inside a query value can be encoded without becoming a path separator.
A literal percent sign also needs attention. The text 25% off becomes 25%25%20off under component-style encoding. The first 25 remains ordinary text; %25 carries the actual percent sign.

Reserved Characters, Unreserved Characters, and UTF-8
RFC 3986 defines unreserved characters as English letters, decimal digits, hyphen, period, underscore, and tilde:
A–Z a–z 0–9 - . _ ~These generally do not need percent-encoding under its generic syntax. Reserved characters include delimiters such as :, /, ?, #, &, and =. Whether they need escaping depends on whether they serve as syntax or as data within the component.
Modern browser behavior is further defined by the WHATWG URL Standard, with different encoding rules for different URL parts and form data. A generic “encode all special characters” instruction is therefore too broad.
Different URL components use different allowed-character sets, and libraries can serialize equivalent values differently. For example, RFC 3986 lists tilde as unreserved, but URLSearchParams serializes a tilde as %7E under its form rules. It leaves an asterisk literal, while Python's default quote() encodes it. These differences do not mean one library is broken.
If an API signs the exact query string, use its specified serializer and canonicalization rules. Two strings can recover the same parameter values yet have different byte-for-byte representations. Sorting parameters, changing spaces from %20 to +, or changing escape style after signing may invalidate that signature.
For non-ASCII text, encode the character as UTF-8 and escape each required byte. The letter ñ becomes bytes C3 B1, giving %C3%B1. Thus, mañana becomes ma%C3%B1ana under the component encoder used here.
Do not write %F1 for that character when the receiver expects UTF-8. F1 is its Latin-1 byte value, not its UTF-8 representation. Correct hexadecimal syntax alone does not guarantee that the resulting byte sequence uses the right character encoding.
Why a Space Can Be %20 or a Plus Sign
Ordinary percent-encoding represents a space as %20. The application/x-www-form-urlencoded convention, used by HTML forms and many query parsers, represents a space as + instead.
A literal plus sign in form data must therefore be escaped as %2B. Otherwise, the parser can turn it into a space.
Original Value | Component-Style Encoding | Form Encoding |
|
|
|
|
|
|
|
|
|
The plus-to-space rule is not universal URL behavior. A + in a path is not automatically a space. Likewise, JavaScript's decodeURIComponent("blue+sky") returns blue+sky, while a form-style query parser reads that value as blue sky.
The distinction also affects how you construct a parser. Passing the serialized string tag=A+B to URLSearchParams yields the value A B. Setting the raw value A+B through .set("tag", "A+B") yields serialized tag=A%2BB instead. The string constructor reads existing query syntax; the setter accepts data. Choose based on what you actually have, rather than treating both inputs as interchangeable.
Avoid globally replacing plus signs across a complete URL. Parse the relevant component with a library that implements the convention your application expects.
Build Query Values and Path Segments in JavaScript
Use URL and its searchParams property when you are assembling query parameters. Supply the original, unencoded value:
const link = new URL("https://example.test/catalog");
link.searchParams.set("q", "nuts + seeds & oats");
console.log(link.search);
// ?q=nuts+%2B+seeds+%26+oats
console.log(link.searchParams.get("q"));
// nuts + seeds & oatsThe serializer keeps the literal plus sign distinct from the spaces. It also protects the ampersand inside the value while preserving the query's structural punctuation.
For a single path segment, encodeURIComponent() can encode the segment's data:
const filename = "draft #3.txt";
const path = "/downloads/" + encodeURIComponent(filename);
console.log(path); // /downloads/draft%20%233.txtThe slash before the filename remains a separator. The hash becomes %23, so it stays part of the filename instead of starting a fragment.
encodeURI() preserves many delimiters because it is intended for a URI whose structure is already established. It is not a substitute for encoding an arbitrary query value. For example, it leaves & and # unescaped.
Also, encodeURIComponent() is not an exact “escape everything except RFC unreserved” function: it leaves !, ', (, ), and * unchanged. Follow an API's specific encoding requirements when constructing signed requests or canonical strings.

Choose the Matching Python urllib.parse Function
Python's urllib.parse module separates component quoting from query-string assembly.
Use quote() for a value that needs percent escapes, and quote_plus() for form-style spaces:
from urllib.parse import quote, quote_plus, urlencode, parse_qs
value = "nuts + seeds & oats"
print(quote(value, safe=""))
# nuts%20%2B%20seeds%20%26%20oats
print(quote_plus(value))
# nuts+%2B+seeds+%26+oats
query = urlencode({"q": value})assert parse_qs(query)["q"] == [value]
The safe argument specifies additional characters to leave unescaped. By default, quote() preserves /. Set safe="" when quoting a single value or path segment that must not retain a literal slash. Unreserved letters, digits, and -._~ still remain unchanged.
For a query with several fields, pass a mapping or sequence of key-value pairs to urlencode() rather than joining user values with & yourself. Its default behavior uses quote_plus(). If an API requires %20 for spaces, you can select quote_via=quote.
Decode with the corresponding operation. unquote() handles percent escapes, while unquote_plus() also interprets plus signs as spaces. For a complete query string, parse_qs() or parse_qsl() handles the parameter structure.
Find Double Encoding Without Decoding Blindly
Double encoding occurs when a value that already contains percent escapes is encoded again at the same intended layer. The percent sign becomes %25, leaving the original hexadecimal pair after it.
const once = encodeURIComponent("room 8");
const twice = encodeURIComponent(once);
console.log(once); // room%208
console.log(twice); // room%25208After one decoding operation, room%25208 becomes room%208, not room 8. This often happens when code calls encodeURIComponent() before passing a value to URLSearchParams, which performs its own serialization.
However, %25 does not always indicate a bug. If the intended literal data is %20, its correctly encoded form is %2520. A nested URL used as one query value can also legitimately contain multiple encoding layers.
For a nested example, suppose the inner relative URL is /reports?team=R%26D. Its parameter value represents R&D. Storing that whole inner URL in an outer next parameter produces next=%2Freports%3Fteam%3DR%2526D with a form serializer.
The outer parser should recover /reports?team=R%26D. Only the inner query parser should then recover the team value R&D. Decoding the outer value twice before parsing the inner URL would expose an ampersand too early and could change the inner parameters. Here, the %25 sequence preserves a necessary inner encoding layer rather than proving an error.
Track ownership instead of guessing from appearance. Determine whether a function accepts raw text or already encoded syntax, and let one layer perform the encoding for that component. Do not repeatedly decode until the string “looks readable”; later decoding may turn data into meaningful delimiters.
Decode Components in Order and Check Failures
Parse the URL's components before decoding their data. Decoding the complete string first can expose %26, %3F, or %23 as punctuation before you have identified the original boundaries.
JavaScript's decodeURIComponent() rejects a malformed escape such as %G2 or a sequence that is not valid UTF-8. Handle the error rather than deleting the percent sign:
try {
decodeURIComponent("%C3");
} catch (error) {
console.log(error.name); // URIError
}Python's unquote() defaults to replacing invalid UTF-8 sequences. Use errors="strict" if invalid encoded text should fail. That option does not itself validate every percent-escape triplet; malformed syntax needs a separate check when strict rejection is required.
A useful test checks the original component against the value recovered by the matching parser:
from urllib.parse import quote, unquote
original = "mañana #4"
encoded = quote(original, safe="")assert encoded == "ma%C3%B1ana%20%234"
assert unquote(encoded, errors="strict") == original
When checking an online URL encoder, select whether it processes a full URL, a component, or form data. Record the original input and the selected mode. Comparing two tools without matching those settings can make correct results appear inconsistent. Use a small sample before processing a long link.
Include spaces, literal plus signs, percent signs, non-ASCII letters, and delimiters in your test data. For paths, also test the server's handling of encoded slashes; browsers, proxies, frameworks, and routers do not always accept or process them identically.
Answers to Common Percent-Encoding Questions
What does %20 mean in a URL?
%20 represents a byte with hexadecimal value 20, which is the ASCII space. It often appears in paths or component-encoded values containing spaces. The sequence is a representation of data, not three characters that the application should display as the original value. Use the appropriate component decoder to recover the space.
Is + always the same as %20?
No. Form-style parsers treat + as a space, so both representations can produce a space in that context. A generic percent decoder does not assign that special meaning to plus signs. In a path, a literal + normally remains a plus. Encode a literal plus as %2B when using form encoding.
Should I use encodeURI or encodeURIComponent?
Use encodeURIComponent() for an individual value that must not introduce URL delimiters. encodeURI() preserves more URL punctuation and is intended for a URI whose structure is already correct. When building query parameters, URLSearchParams is usually simpler because it handles names, values, separators, and form-style spaces together.
Why does my URL contain %2520 instead of %20?
The percent sign in an existing %20 may have been encoded again as %25, producing %2520. Check whether a value was manually encoded before entering a serializer. It can also be intentional when the literal data contains the text %20 or when one encoded URL is nested inside another parameter.
Can I replace %2F with a slash?
Do not assume that replacement is harmless. A slash can separate path segments, while an encoded slash may be treated as data, normalized, rejected, or decoded by a routing layer. Follow the application's routing contract. In query values, use a query parser rather than applying replacements across the full URL.
Does uppercase or lowercase hex change a percent escape?
No. %2F and %2f represent the same byte under the generic percent-encoding rules. Uppercase hexadecimal output is recommended for consistency. That does not make every part of a URL case-insensitive: path names and parameter values can still differ by letter case according to the application's rules.
Is percent-encoding encryption or protection for private data?
No. Anyone can reverse percent escapes using the correct decoding rules. Encoded values can remain visible in URLs, browser history, logs, and copied links. Encoding prevents data from being misread as URL syntax; it does not provide confidentiality. Keep secrets out of URLs rather than relying on escapes to hide them.
Encode at the Boundary Where Data Becomes a URL
Reliable URL and percent hexadecimal encoding starts with the component, not with a global replacement rule. Pass original values to the right serializer, keep structural separators intact, and decode with the matching convention. When a link breaks, inspect the component boundaries and encoding layers before changing individual percent escapes.