Encode and decode a string
Python’s Base64 functions work on bytes, so text has to be encoded to UTF-8 first and decoded back at the end. Forgetting either step is the most common mistake.
import base64
s = "héllo wörld"
encoded = base64.b64encode(s.encode("utf-8")).decode("ascii")
print(encoded) # aMOpbGxvIHfDtnJsZA==
decoded = base64.b64decode(encoded).decode("utf-8")
print(decoded) # héllo wörld
b64decode accepts both str and bytes, but b64encode only takes bytes-like objects. Passing a string raises TypeError: a bytes-like object is required, not 'str'.
To check a result by hand, paste it into the Base64 decoder, which runs locally in the browser.
URL-safe Base64 with and without padding
The URL-safe alphabet replaces + with - and / with _ (see Base64 vs Base64URL). Python keeps the = padding; strip it yourself if the target format (JWT, many APIs) wants none.
data = b"<<???>>"
print(base64.b64encode(data)) # b'PDw/Pz8+Pg=='
print(base64.urlsafe_b64encode(data)) # b'PDw_Pz8-Pg=='
print(base64.urlsafe_b64encode(data).rstrip(b"=")) # b'PDw_Pz8-Pg'
Decoding unpadded input needs the padding put back, because Python insists on a length that is a multiple of 4:
def b64url_decode(s: str) -> bytes:
return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
print(b64url_decode("PDw_Pz8-Pg")) # b'<<???>>'
b64encode and b64decode also take an altchars argument, a two-byte string that replaces +/. altchars=b"-_" is equivalent to the URL-safe functions.
print(base64.b64decode("PDw_Pz8-Pg==", altchars="-_")) # b'<<???>>'
Encode and decode a file
For files that fit in memory, read the bytes and encode them in one call:
from pathlib import Path
b64 = base64.b64encode(Path("hello.txt").read_bytes()).decode("ascii")
print(b64) # aGVsbG8K
Path("hello-copy.txt").write_bytes(base64.b64decode(b64))
For a one-off conversion without writing code, the file to Base64 tool does the same thing in the browser.
Large files and streaming
Base64 maps every 3 input bytes to 4 output characters, so you can encode a stream in chunks as long as each chunk except the last is a multiple of 3 bytes. Then no padding appears in the middle and the concatenated output equals a single-call encode.
CHUNK = 3 * 1024 * 256 # 768 KiB, a multiple of 3
with open("big.bin", "rb") as src, open("big.b64", "wb") as dst:
while chunk := src.read(CHUNK):
dst.write(base64.b64encode(chunk))
We checked this against base64 -w 0 on a 100 KB random file: byte-for-byte identical. Decoding in chunks works the same way with chunk sizes that are a multiple of 4 characters, provided the input has no line breaks.
MIME line wrapping
Email and some legacy systems want lines of at most 76 characters (RFC 2045). encodebytes does that, and base64.encode / base64.decode do it between file objects:
print(base64.encodebytes(b"x" * 60))
# b'eHh4eHh4...eHh4\neHh4\n' (76 chars, newline, rest, newline)
with open("big.bin", "rb") as src, open("big.mime", "wb") as dst:
base64.encode(src, dst)
with open("big.mime", "rb") as src, open("big.out", "wb") as dst:
base64.decode(src, dst)
Note the trailing newline: encodebytes always adds one. For an online version, see the MIME Base64 encoder.
Validating input
By default b64decode is lenient: it throws away any character outside the alphabet before decoding. That hides bugs. Pass validate=True to make it strict.
import binascii
print(base64.b64decode("aGk=!!")) # b'hi' (junk ignored)
try:
base64.b64decode("aGk=!!", validate=True)
except binascii.Error as e:
print(e) # Only base64 data is allowed
With validate=True, newlines also count as invalid, so strip MIME line breaks first. The errors you will see, all of type binascii.Error (a subclass of ValueError):
| Input | Message |
|---|---|
aGk | Incorrect padding |
a | Invalid base64-encoded string: number of data characters (1) cannot be 1 more than a multiple of 4 |
aGk== with validate=True | Excess padding not allowed |
aGk=!! with validate=True | Only base64 data is allowed |
The Base64 errors guide explains what causes each of these, and the Base64 validator shows where a string breaks.
Common pitfalls
URL-safe input fed to b64decode. Without validate=True, the - and _ characters are silently dropped, and you get garbage instead of an error:
print(base64.b64decode("PDw_Pz8-Pg==")) # b'<<\x0f\xcf\xc3\xe0'
print(base64.urlsafe_b64decode("PDw_Pz8-Pg==")) # b'<<???>>'
urlsafe_b64decode accepts standard input too, so it is the safer default when you are not sure which alphabet you received.
Decoding binary as text. If the payload is an image or a ZIP, .decode("utf-8") fails with UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte. Keep it as bytes and write it to a file.
The b'...' in output. Printing the return value of b64encode shows a bytes literal. If that b' ends up in JSON or a header, you forgot .decode("ascii").
Treating Base64 as protection. It is an encoding, and anyone can reverse it. See encoding vs encryption.
Version notes
base64.b64encode,b64decode,urlsafe_*andaltcharsexist in every Python 3 release.b64decodehas accepted ASCIIstrinput (not only bytes) since Python 3.3.encodestringanddecodestringwere removed in 3.9; useencodebytesanddecodebytes.binascii.b2a_base64(data, newline=False)(3.6+) is the low-level function behindb64encodeand returnsb'aGk='forb"hi".- The same module also has
b32encode,b16encode,b85encodeanda85encodefor other alphabets.
FAQ
Why does b64encode return b'...' instead of a string?
Base64 functions in Python take bytes and return bytes. Call .decode("ascii") on the result to get a str, and .encode("utf-8") on text before encoding it.
How do I decode Base64 that has no padding in Python?
Add the missing = back before decoding: s + "=" * (-len(s) % 4). Python refuses unpadded input with binascii.Error: Incorrect padding.
What is the difference between b64decode and urlsafe_b64decode?
urlsafe_b64decode translates - and _ to + and / before decoding. Plain b64decode silently drops - and _ as junk, so you get wrong bytes instead of an error unless you pass validate=True.
Is base64.encodestring still available?
No. encodestring and decodestring were removed in Python 3.9. Use encodebytes and decodebytes for MIME-style output with line breaks.