Fix Python binascii.Error: Incorrect Padding
Python raises binascii.Error: Incorrect padding because base64.b64decode() received a string whose length is not a multiple of 4 and is missing trailing = characters. Restore the padding before decoding with padded = s + "=" * (-len(s) % 4), then decode padded. That single line repairs the most common cause instantly.
Why this error happens
Base64 encodes every 3 bytes of input as 4 ASCII characters. When the input length is not divisible by 3, the encoder appends one or two = characters so the output length stays a multiple of 4. If those trailing = get stripped somewhere in transit, Python can no longer tell where the data ends, and b64decode aborts with Incorrect padding.
Padding loss is extremely common. JWTs and OAuth tokens deliberately drop padding. URL query strings, JSON serializers, and copy-paste from logs often trim trailing = because it looks like noise. A string of 271 characters, for example, leaves a remainder of 3 when divided by 4, so it always fails until you add a single =.
The padding fix
Append exactly as many = as needed to round the length up to the next multiple of 4. The expression -len(s) % 4 evaluates to 0, 1, 2, or 3 and is a no-op when the string is already correctly padded, so it is safe to apply unconditionally.
import base64
def b64decode_safe(s: str) -> bytes:
if isinstance(s, bytes):
s = s.decode("ascii")
padded = s + "=" * (-len(s) % 4)
return base64.b64decode(padded)
b64decode_safe("aGVsbG8gd29ybGQ") # b'hello world'
You do not need to compute 4 - (len(s) % 4) and special-case the zero remainder. The modulo-of-negative trick handles all four cases in one expression. With the default settings, b64decode also tolerates a few extra =, so over-padding by accident will not break anything.
Strip prefixes and stray characters
Padding is only half the story. If the input contains characters outside the Base64 alphabet, you can still fail even when the = count is correct. Two cases dominate.
Data URI prefixes
Browser FileReader and many APIs hand you a full data URI such as data:image/png;base64,iVBORw0KGgo.... The leading data:...;base64, is not Base64 and must be removed before decoding.
raw = "data:image/png;base64,iVBORw0KGgo="
if "," in raw:
raw = raw.split(",", 1)[1]
img_bytes = base64.b64decode(raw + "=" * (-len(raw) % 4))
Whitespace and newlines
PEM blocks, email MIME bodies, and pretty-printed payloads wrap Base64 across lines. By default b64decode discards non-alphabet characters before the padding check, so embedded newlines usually pass. But invisible characters like a UTF-8 BOM, zero-width spaces, or smart quotes pasted from a document can sneak through and corrupt the result, and they often masquerade as a padding error. A regex scrub removes them:
import re
text = "aGVsbG8g\nd29ybGQ="
cleaned = re.sub(r"[^A-Za-z0-9+/=]", "", text)
decoded = base64.b64decode(cleaned) # b'hello world'
URL-safe Base64 and tokens
JWTs, OAuth tokens, and anything passed in a URL use the URL-safe alphabet: + becomes - and / becomes _. If you feed such a string to the standard b64decode without help, the - and _ are silently discarded under the default validate=False, so you decode the wrong bytes rather than seeing an obvious error. Use urlsafe_b64decode, which still needs padding restored manually.
import base64
import json
def decode_jwt_segment(seg: str) -> bytes:
seg += "=" * (-len(seg) % 4)
return base64.urlsafe_b64decode(seg)
# Decode the payload (middle segment) of a JWT
header, payload, sig = token.split(".")
claims = json.loads(decode_jwt_segment(payload))
If you only need to read claims interactively, paste the token into our JWT decoder, which splits the segments and handles the URL-safe alphabet and padding for you. For raw Base64 round-trips while debugging, the Base64 encoder and decoder is the fastest sanity check, and the URL encoder helps when a token has been percent-encoded on top of being Base64.
The validate=False quirk
The full signature is base64.b64decode(s, altchars=None, validate=False). The validate flag controls strictness, and its asymmetric behavior surprises people.
- With
validate=False(the default), characters outside the alphabet are silently discarded before the padding check. Missing padding still raisesIncorrect padding, but redundant trailing=are quietly accepted. - With
validate=True, any non-alphabet character raisesbinascii.Error: Only base64 data is allowed, and extra=beyond what is needed raisesbinascii.Error: Excess data after padding, instead of being ignored.
base64.b64decode("aGVsbG8gd29ybGQ======") # b'hello world' (extra = ignored)
base64.b64decode("aGVsbG8gd29ybGQ======", validate=True) # binascii.Error: Excess data after padding
base64.b64decode("aGVsbG8gd29ybGQ") # binascii.Error: Incorrect padding
The practical takeaway: for forgiving decoding of messy real-world input, keep validate=False and restore padding yourself. For integrity checks where any corruption should fail loudly, set validate=True and pass an exactly-padded string. To decode standard-alphabet data that uses custom characters, pass altchars=b"-_" rather than switching functions.
A robust decoder for any source
Combine the techniques into one function that handles tokens, data URIs, and stray characters, defaulting to the URL-safe alphabet so JWTs work without a separate path.
import base64, re
def universal_b64decode(s, urlsafe=True) -> bytes:
if isinstance(s, bytes):
s = s.decode("ascii", "ignore")
if "base64," in s: # drop data: URI prefix
s = s.split("base64,", 1)[1]
s = re.sub(r"[^A-Za-z0-9+/_=-]", "", s) # strip whitespace/junk
s += "=" * (-len(s) % 4) # restore padding
return (base64.urlsafe_b64decode if urlsafe
else base64.b64decode)(s)
One caveat for the future: Python 3.15 tightens these rules. urlsafe_b64decode moves toward not auto-padding, non-ASCII input is rejected under validate=False, and mixing altchars with validate=False emits a FutureWarning. Restoring padding explicitly, as shown above, keeps your code working across versions rather than relying on lenient defaults that are being phased out.
Frequently Asked Questions
It means base64.b64decode received a string whose length is not a multiple of 4 and is missing its trailing = padding characters. Base64 output is always a multiple of 4 characters, so a non-multiple length signals lost padding.
Append the correct number of = characters with padded = s + '=' * (-len(s) % 4), then decode padded. That expression adds 0, 1, 2, or 3 equals signs as needed and does nothing when the string is already valid.
JWTs strip padding and use the URL-safe alphabet where + is - and / is _. Restore the padding manually and decode with base64.urlsafe_b64decode rather than the standard b64decode, which silently discards the URL-safe characters and returns the wrong bytes.
With the default validate=False, non-alphabet characters and extra trailing = are silently discarded, but missing padding still errors. With validate=True, a non-alphabet character raises 'Only base64 data is allowed' and excess = raises 'Excess data after padding'.