Encoding troubleshooting · Updated October 19, 2026
Base32hex “Non-base32 digit found”: Fix the Alphabet Mismatch
If Base32hex text raises binascii.Error: Non-base32 digit found in Python's b32decode, use b32hexdecode instead. For example, D1IMOR3F is Base32hex for hello. Changing padding cannot repair the wrong alphabet. Also check bytes after a successful decode: CPNMU=== silently decodes to the wrong bytes with the standard Base32 decoder.
Reproduce the actual exception
This failure starts with an already encoded string, not with text waiting for a base 32 encoder. The producer chose the extended-hex alphabet, while the consumer chose the standard alphabet. Run this with Python 3.10 or later, which provides the Base32hex APIs:
import base64
encoded = base64.b32hexencode(b"hello")
print(encoded) # b'D1IMOR3F'
base64.b32decode(encoded) # wrong decoder
# binascii.Error: Non-base32 digit foundThe digit 1 is legitimate Base32hex data. The standard Base32 alphabet does not include it. The two APIs operate on the same five-bit group size but assign different characters to the numeric values. Replacing a character until the error disappears is not a safe repair.
The repair: match the producer's alphabet
import base64
encoded = b"D1IMOR3F"
raw = base64.b32hexdecode(encoded)
assert raw == b"hello"
assert base64.b32hexencode(raw) == encoded
print(raw.hex()) # 68656c6c6fUse the declared format from your API contract, field description, or producing code. A successful byte round trip is useful evidence that your selected implementation behaves consistently. It does not by itself prove the producer used that alphabet: for ambiguous input, both decoders can accept the same characters and return different bytes.
Our decoder-swap matrix
We generated each string from the exact bytes in the first column using Python's standard library. Then we deliberately passed the Base32hex result to both decoders. The wrong-decoder column records either the returned hex bytes or the exception message, without replacing invalid bytes with display characters. This distinguishes a visible rejection from silent data corruption.
| Input hex | Standard Base32 | Base32hex | Wrong standard decoder | Correct hex decoder |
|---|---|---|---|---|
666f6f | MZXW6=== | CPNMU=== | 13daca | 666f6f |
68656c6c6f | NBSWY3DP | D1IMOR3F | Error: Non-base32 digit found | 68656c6c6f |
666f6f626172 | MZXW6YTBOI====== | CPNMUOJ1E8====== | Error: Non-base32 digit found | 666f6f626172 |
00 | AA====== | 00====== | Error: Non-base32 digit found | 00 |
ff | 74====== | VS====== | ac | ff |
e4b8ad | 4S4K2=== | SISAQ=== | 922408 | e4b8ad |
The foo row is the important trap. Its Base32hex output contains only characters also accepted by the standard alphabet. The wrong call therefore returns 13daca instead of raising an exception. If that byte sequence is later interpreted as UTF-8, a text error may appear downstream, hiding the earlier alphabet mismatch. Keep the comparison at the byte level until the encoding format is settled.
Re-run the matrix on your own machine
import base64
import binascii
for raw in [b"foo", b"hello", b"foobar", bytes([0]), bytes([255])]:
encoded = base64.b32hexencode(raw)
try:
wrong = base64.b32decode(encoded).hex()
except binascii.Error as exc:
wrong = type(exc).__name__ + ": " + str(exc)
correct = base64.b32hexdecode(encoded)
assert correct == raw
print(raw.hex(), encoded.decode("ascii"), wrong, correct.hex())Our additional round-trip check covers lengths zero through 256 bytes, with deterministic byte sequences. That check verifies correct-alphabet decoding across length boundaries; it does not claim to test every possible input or every language's decoder. Error wording on this page is the observed Python wording, not a promise that other libraries report the same message.
Why the common quick fixes miss this failure
Adding or deleting equals signs
Padding marks an incomplete block. It does not change the alphabet's character-to-number mapping. In the hello example there are five input bytes and no padding at all, yet the wrong decoder still rejects the result. For a separate padding problem, use the Base32 padding and URL parameter guide; that page addresses lengths rather than alphabet selection.
Turning on case folding or mapping 0 and 1
Accepting lowercase characters is a different policy from choosing Base32hex. Likewise, interpreting look-alike digits as letters does not select the extended-hex alphabet. Base32hex assigns real values to its digits; substituting visually similar characters can change the data. Fix the decoder selection before relaxing any input policy.
Percent-encoding the failing string
URL escaping protects a value while it travels inside a URL. It does not repair the underlying Base32 format. Parse the URL parameter once, obtain the original encoded string, and then decode that string with the correct alphabet. Use the URL encoder and decoder only for that transport layer. If the input contains literal %3D after URL parsing, investigate double encoding separately.
A short debugging order
- Save a non-secret failing value and the exact exception.
- Find the producer's format declaration: standard Base32 or Base32hex.
- Select the matching decoder without changing the input.
- Compare the returned bytes with a known expected fixture.
- Only then decode bytes as text, if the field is actually text.
This page deliberately does not guess an alphabet from appearance. A string containing only overlapping characters is ambiguous, as the successful-but-wrong row demonstrates. When the format is unspecified, fix the contract rather than shipping an auto-detection rule that silently chooses one interpretation.
API reference: Python Base32hex decoding. The decoder-swap matrix and failure analysis above are original reproducible experiments.