Base64 in Python
base64.b64encode takes bytes, not str, and raises TypeError otherwise. Python also rejects unpadded input that JavaScript accepts — the two facts account for most Base64 bugs in Python.
By the Withuse team · Updated
Every snippet and every output below was executed on Python 3.13.
Encoding text
import base64
text = "Hello, 안녕 🎉"
# ✗ TypeError: a bytes-like object is required, not 'str'
base64.b64encode(text)
# ✓
base64.b64encode(text.encode("utf-8")).decode()
# 'SGVsbG8sIOyViOuFlSDwn46J'The two conversions look redundant and are not. The inner .encode("utf-8") turns text into the bytes Base64 is defined over; the outer .decode() turns the Base64 output — which is also bytes — back into an ASCII string. Skip the second and you end up putting b'SGVsbG8…' into a JSON payload, complete with the b prefix and quotes.
We checked that this produces exactly the same string as the JavaScript and Node paths in our Base64 in JavaScript guide. That is worth knowing: when two services disagree about a Base64 value, the encoding is almost never the culprit.
Python fails loudly where JavaScript fails silently
JavaScript's btoa also breaks on non-Latin-1 text, but the failure modes differ in a way that favours Python. Python refuses at the call because the types are wrong. JavaScript accepts a string and throws only once it meets a character above 255, which means the same code can work in testing with ASCII and fail in production on the first accented name.
URL-safe encoding, and a coincidence that hides bugs
base64.b64encode(b) # uses + and / base64.urlsafe_b64encode(b) # uses - and _ # for our sample both produced the identical string, # because no + or / appeared in the output at all
The two functions differ only in the characters chosen for values 62 and 63. For a great many inputs neither character occurs, so the outputs match and a mismatched pair of functions appears to work. The bug then surfaces on one particular payload, long after the code shipped. If a value will ever sit in a URL, use the URL-safe function from the start rather than waiting to be surprised.
Both still append = padding, which the canonical base64url form omits. Strip it yourself when producing tokens: base64.urlsafe_b64encode(b).rstrip(b"=").
The padding difference that catches everyone
base64.b64decode("YQ==") # b'a'
base64.b64decode("YQ") # binascii.Error: Incorrect padding
# JavaScript, for comparison
atob("YQ") // "a" <- acceptedPython enforces the padding rule; the browser does not. Since base64url values normally arrive without padding, feeding a JWT segment straight to b64decode fails on the first try. Restore it first:
s += "=" * (-len(s) % 4) base64.urlsafe_b64decode(s)
Use validate=True on untrusted input
By default b64decode silently discards any character outside the Base64 alphabet, a tolerance inherited from email formats that wrapped lines. The consequence today is that a wrong-variant or corrupted string decodes into plausible garbage instead of raising. We confirmed that a value containing a hyphen is quietly accepted by default and rejected under validate=True.
For anything arriving from a client, an API or a file you did not write, pass validate=True. A silent wrong answer is far more expensive than an exception.
Frequently asked questions
Why does base64.b64encode raise TypeError?
Because it operates on bytes and you passed a str. Python 3 keeps text and binary strictly apart, and Base64 is defined over bytes, so the function refuses text outright with "a bytes-like object is required, not 'str'" — we confirmed the exact message. The fix is to encode first: base64.b64encode(text.encode("utf-8")). The return value is bytes too, so add .decode() if you want a str back. This is Python's version of the trap JavaScript has with btoa, but it fails loudly at the call rather than silently mangling non-ASCII input, which is the better failure. Python 2 blurred the two types and let this pass, which is why old snippets found online often omit the encode step and then break under Python 3.
How do I Base64 encode a string in Python?
Encode to UTF-8 bytes, Base64 those bytes, then decode the result back to text: base64.b64encode(text.encode("utf-8")).decode(). We verified that "Hello, 안녕 🎉" produces SGVsbG8sIOyViOuFlSDwn46J, byte-identical to what JavaScript and Node produce for the same input. The two .encode/.decode calls look redundant but do different jobs — the first turns text into bytes, the second turns the Base64 bytes back into an ASCII string. Skipping the second leaves you with b'SGVsbG8...' which will surprise you in a JSON payload or a log line. Both b64encode and b64decode return bytes regardless of what you passed in, so the outer decode is needed on every call whose result becomes text rather than staying in a bytes pipeline.
What is the difference between b64encode and urlsafe_b64encode?
They differ only in the two characters used for values 62 and 63. The standard function emits + and /, both of which have meaning inside a URL; urlsafe_b64encode substitutes - and _ instead, matching RFC 4648 §5. Both still pad with =, which the standard base64url form usually omits, so strip it yourself with .rstrip(b"=") when producing tokens. Note that for many inputs the two outputs are identical — ours were — because + and / only appear for particular byte patterns. That coincidence is exactly why the bug hides until a specific payload triggers it. The two characters appear only for particular byte patterns, so test data made of ASCII will almost never surface the difference.
Why does Python reject padding that JavaScript accepts?
Because base64.b64decode enforces the padding rule and atob does not. We measured both: b64decode("YQ") raises binascii.Error with "Incorrect padding", while the browser's atob("YQ") happily returns "a". Python is following the specification more strictly. The practical consequence is that base64url values, which normally drop their padding, cannot be handed straight to b64decode — you must add the equals signs back first. A common idiom is s += "=" * (-len(s) % 4). If you are debugging a JWT segment in Python, this is almost always the error you hit first, and the fix is one line rather than a library. Use urlsafe_b64decode for those values as well, since the alphabet differs too.
Should I use validate=True when decoding?
Yes, whenever the input comes from outside your own code. By default b64decode silently discards any character that is not in the Base64 alphabet, so a corrupted or wrong-variant string decodes into plausible-looking garbage rather than raising. Passing validate=True makes it reject those characters instead — we confirmed that a base64url string containing a hyphen raises under validate=True and is quietly accepted without it. The default exists for historical tolerance of line breaks in email, but for API input the strict behaviour is what you want, because a silent wrong answer costs far more than an exception. The tolerant default dates from email formats that wrapped Base64 across lines, a problem no modern API has.
References
Check a value against the Base64 encoder and decoder, or read the browser-side story in Base64 in JavaScript. More tools at withuse.io/tools.