How to Base64 Encode and Decode in Python
Python's base64 module is straightforward once you internalize one rule: it operates on bytes, not strings. Calling base64.b64encode("hello") throws TypeError, because the function expects b"hello". This guide covers the right way to encode and decode Base64 in Python — text, files, URL-safe variants, padding handling — with code you can copy directly. Verify any output against the Base64 encoder/decoder tool.
The Module
Everything Base64-related in Python lives in the standard base64 module — no pip install needed. The main functions:
b64encode(bytes)→bytes— standard Base64 (uses+,/,=)b64decode(bytes_or_str)→bytes— standard Base64 decodeurlsafe_b64encode(bytes)→bytes— URL-safe Base64 (uses-,_,=)urlsafe_b64decode(bytes_or_str)→bytes— URL-safe Base64 decode
The single rule that trips most newcomers: every function takes and returns bytes, not strings. Encoding a string requires .encode("utf-8") first; decoding back to a string requires .decode("utf-8") after.
Encoding and Decoding Text
import base64
# Encode a string
encoded = base64.b64encode("hello world".encode("utf-8"))
# b'aGVsbG8gd29ybGQ='
# Often you want it as a str, not bytes
encoded_str = base64.b64encode("hello world".encode("utf-8")).decode("ascii")
# 'aGVsbG8gd29ybGQ='
# Decode back to a string
decoded = base64.b64decode("aGVsbG8gd29ybGQ=").decode("utf-8")
# 'hello world'
Unicode just works because .encode("utf-8") handles every character, including emoji:
base64.b64encode("héllo 👋".encode("utf-8")).decode("ascii")
# 'aMOpbGxvIPCfkYs='
base64.b64decode("aMOpbGxvIPCfkYs=").decode("utf-8")
# 'héllo 👋'
Unlike JavaScript's btoa, Python's b64encode doesn't care about character ranges — it just encodes the bytes you give it. The Unicode handling is done by .encode("utf-8"), which is a separate, explicit step.
URL-Safe Base64
For URLs, query strings, JWTs, and filenames, use urlsafe_b64encode:
import base64
# URL-safe encode
url_safe = base64.urlsafe_b64encode("hello?world".encode()).decode("ascii")
# 'aGVsbG8_d29ybGQ=' (note _ instead of /)
# URL-safe decode
decoded = base64.urlsafe_b64decode("aGVsbG8_d29ybGQ=").decode()
# 'hello?world'
JWTs use URL-safe Base64 with no padding. To match the JWT convention, strip the = after encoding and add it back before decoding:
def jwt_b64encode(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
def jwt_b64decode(s: str) -> bytes:
# Pad to a multiple of 4 with '='
padded = s + "=" * (-len(s) % 4)
return base64.urlsafe_b64decode(padded.encode("ascii"))
The -len(s) % 4 trick is the cleanest way to compute "how many = do I need to make the length a multiple of 4."
Encoding Files
For files under a few megabytes, read the whole thing and encode it in one call:
import base64
with open("image.png", "rb") as f:
b64 = base64.b64encode(f.read()).decode("ascii")
# Write it back
with open("image_copy.png", "wb") as f:
f.write(base64.b64decode(b64))
For large files, stream in chunks. Base64 encodes 3 bytes at a time into 4 characters, so chunk sizes should be multiples of 3 to avoid padding in the middle of the output:
import base64
CHUNK_SIZE = 3 * 1024 * 1024 # 3 MB — multiple of 3
with open("big.bin", "rb") as src, open("big.b64", "w") as dst:
while True:
chunk = src.read(CHUNK_SIZE)
if not chunk:
break
dst.write(base64.b64encode(chunk).decode("ascii"))
The base64 module also has a built-in file-streaming helper:
import base64
# Encodes input file to output file, automatically chunked
with open("big.bin", "rb") as src, open("big.b64", "wb") as dst:
base64.encode(src, dst)
# And the reverse
with open("big.b64", "rb") as src, open("big.bin", "wb") as dst:
base64.decode(src, dst)
base64.encode also inserts line breaks every 76 characters (MIME convention), which is fine for email attachments but might be unwanted elsewhere — strip with data.replace(b"\n", b"") if needed.
Other Encodings in the Same Module
The base64 module is more than just Base64. The other useful encodings:
import base64
data = b"hello"
base64.b64encode(data) # b'aGVsbG8=' standard Base64
base64.urlsafe_b64encode(data) # b'aGVsbG8=' URL-safe Base64
base64.b32encode(data) # b'NBSWY3DP' Base32 (case-insensitive)
base64.b32hexencode(data) # b'D1IMOR3F' Base32 with extended hex alphabet
base64.b16encode(data) # b'68656C6C6F' Base16 (uppercase hex)
base64.a85encode(data) # b'BOu!rDZ' ASCII85 (PostScript/PDF)
base64.b85encode(data) # b'Xk~0{Zy' Base85 (RFC 1924, more compact)
Base32 is what TOTP secret keys use (the long string you scan into Authy or Google Authenticator). Base16 is just uppercase hex — useful when interoperating with systems that expect that format. Base85 is denser (4 bytes → 5 characters instead of 3 → 4), which is why PDFs use it internally for embedded data.
Common Pitfalls
TypeError: a bytes-like object is required, not 'str'
The most common Base64 error in Python. You called b64encode("hello") instead of b64encode(b"hello") or b64encode("hello".encode()). Always pass bytes.
binascii.Error: Invalid base64-encoded string
The input has wrong padding, invalid characters, or wrong length. Common causes: missing = at the end (add padding back with data + "=" * (-len(data) % 4)), wrong alphabet (URL-safe input passed to standard decoder, or vice versa), or whitespace in the middle (strip with data.replace(b"\n", b"").replace(b" ", b"")).
UnicodeDecodeError after decoding
Base64 decoding returns bytes; if you call .decode("utf-8") on bytes that aren't valid UTF-8, you get UnicodeDecodeError. This means the original input wasn't UTF-8 text — it was binary data, or text in a different encoding, or the Base64 itself was corrupt. For binary data, don't decode at all (keep the bytes). For non-UTF-8 text, use the correct encoding name.
Line breaks in MIME-style Base64
Output from base64.encode (the streaming version) includes line breaks every 76 characters; output from b64encode doesn't. PEM files, email attachments, and HTTP Authorization: Basic headers all expect no line breaks. b64encode is the right choice for those; strip line breaks from encode output if needed.
Try It Live
Use the Base64 encoder/decoder to verify any output from the snippets above — the tool's encoding matches Python's b64encode exactly. The What is Base64 guide covers the format itself; the JavaScript Base64 guide covers the same patterns in JS for cross-language work.
Frequently Asked Questions
Why does Python b64encode require bytes and not strings?
Base64 is fundamentally a byte-to-text encoding: it takes arbitrary binary data and produces an ASCII-safe representation. Python 3 makes the distinction between text (str) and bytes (bytes) explicit — str is a sequence of Unicode code points, bytes is a sequence of byte values 0-255. Since Base64 operates on bytes, b64encode only accepts bytes. To encode a string, encode it to bytes first with .encode("utf-8"): base64.b64encode("hello".encode("utf-8")). To decode back to a string, decode the bytes after: base64.b64decode(b64).decode("utf-8").
What is the difference between b64encode and urlsafe_b64encode?
Both produce 64-character Base64, but they use different alphabets for two of the characters. Standard b64encode uses + and /, plus = for padding — all three have special meaning in URLs and filenames. urlsafe_b64encode substitutes - for + and _ for /, producing strings that are safe to drop into URL query parameters, JWTs, or filenames without further escaping. Padding still uses =; strip it manually if your downstream consumer expects unpadded URL-safe Base64 (JWTs always strip padding).
How do I Base64-encode a file in Python?
For small files (under ~10MB), read the whole file as bytes and encode it in one call: with open("img.png", "rb") as f: b64 = base64.b64encode(f.read()).decode("ascii"). For larger files, stream in chunks of a multiple of 3 (Base64 encodes 3 bytes to 4 characters) to avoid padding in the middle: chunk_size = 3 * 1024 * 1024 # 3MB chunks, then loop through the file. The base64.encode(input_fp, output_fp) function does this for you with file objects.
How do I handle Base64 strings without padding?
Standard b64decode requires correct padding (= at the end so the total length is a multiple of 4). For unpadded input (common in JWTs and some web APIs), add the padding back before decoding: data + "=" * (-len(data) % 4) pads to the next multiple of 4. Alternatively, pass validate=False (Python 3.x default) which is permissive, but the safer approach is to pad explicitly. b64decode also accepts altchars for arbitrary alphabet substitution, useful for non-standard variants.
What is the difference between Base64, Base64url, and Base32 in Python?
All three are part of the base64 module. Base64 (b64encode/b64decode) uses 64 characters from the standard alphabet plus = padding. Base64url (urlsafe_b64encode/urlsafe_b64decode) uses the same 64 characters but replaces +// with -/_ for URL-safe use. Base32 (b32encode/b32decode) uses only 32 characters (A-Z, 2-7), is case-insensitive, and is human-friendlier — used by TOTP secret keys, magnet URLs, and some DNS names. There's also b16encode (hex), b85encode (Base85, smaller output but uses more special characters), and a85encode (ASCII85 used by PostScript and PDF).