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 found

The 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())                # 68656c6c6f

Use 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.

The same Base32hex string CPNMU decodes to 666f6f with the correct decoder and 13daca with the wrong one.
Acceptance is not validation: compare bytes against a known fixture.

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 hexStandard Base32Base32hexWrong standard decoderCorrect hex decoder
666f6fMZXW6===CPNMU===13daca666f6f
68656c6c6fNBSWY3DPD1IMOR3FError: Non-base32 digit found68656c6c6f
666f6f626172MZXW6YTBOI======CPNMUOJ1E8======Error: Non-base32 digit found666f6f626172
00AA======00======Error: Non-base32 digit found00
ff74======VS======acff
e4b8ad4S4K2===SISAQ===922408e4b8ad

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

  1. Save a non-secret failing value and the exact exception.
  2. Find the producer's format declaration: standard Base32 or Base32hex.
  3. Select the matching decoder without changing the input.
  4. Compare the returned bytes with a known expected fixture.
  5. 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.