Python base64
last modified October 29, 2026 Python 3.15
In this article we show how to encode binary data as text with the
base64 module.
Many channels can carry only text: email, JSON documents, URLs, HTTP headers, XML, or configuration files. Base64 represents arbitrary bytes with 64 printable ASCII characters, so binary data such as images, keys, and signatures can travel through them. The encodings are defined in RFC 4648.
The base64 module implements Base64, Base32, Base16, and the
Base85 family. Python 3.15 added several options: padded to
omit padding, wrapcol to split output into lines,
ignorechars to skip selected characters, and
canonical to reject non-canonical input. The encoders are also
much faster than in earlier versions.
The functions take bytes-like objects and return bytes.
Decoders also accept ASCII strings. For converting between text and bytes,
see Python encode/decode.
Encoding and decoding
The b64encode and b64decode functions work with the
standard Base64 alphabet.
import base64
text = 'Hello, Python!'
encoded = base64.b64encode(text.encode('utf-8'))
print(encoded)
print(encoded.decode('ascii'))
decoded = base64.b64decode(encoded)
print(decoded.decode('utf-8'))
print(base64.b64encode('Žltý kôň'.encode()))
print(base64.b64decode('xb1sdMO9IGvDtMWI').decode())
Base64 works with bytes, not text. We encode the string to UTF-8 bytes
first. The result is a bytes object that contains only ASCII
characters, so we can decode it to a str when we need text.
print(base64.b64decode('xb1sdMO9IGvDtMWI').decode())
Decoding reverses the process; the decoder accepts a string, too. Non-ASCII text works because Base64 sees only the UTF-8 bytes.
$ python first_encode.py b'SGVsbG8sIFB5dGhvbiE=' SGVsbG8sIFB5dGhvbiE= Hello, Python! b'xb1sdMO9IGvDtMWI' Žltý kôň
How Base64 works
Base64 splits the input into groups of three bytes, 24 bits, and splits each group into four 6-bit numbers. Each number from 0 to 63 is an index into the alphabet.
import base64
ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
def encode(data):
out = []
for i in range(0, len(data), 3):
chunk = data[i:i + 3]
n = int.from_bytes(chunk.ljust(3, b'\0'))
chars = [ALPHABET[(n >> shift) & 0x3f] for shift in (18, 12, 6, 0)]
used = len(chunk) + 1
out.append(''.join(chars[:used]) + '=' * (4 - used))
return ''.join(out)
data = b'Man'
print(' '.join(f'{b:08b}' for b in data))
n = int.from_bytes(data)
print(' '.join(f'{(n >> s) & 0x3f:06b}' for s in (18, 12, 6, 0)))
print(encode(data))
for sample in [b'Ma', b'M', b'Many hands make light work.']:
print(encode(sample), encode(sample) == base64.b64encode(sample).decode())
We implement the encoder ourselves and check it against the module. The
classic example Man shows the bits: three 8-bit bytes become four
6-bit indexes, 19, 22, 5, and 46, which are the letters TWFu.
used = len(chunk) + 1
out.append(''.join(chars[:used]) + '=' * (4 - used))
The last group can have only one or two bytes. It is padded with zero bits,
and the missing characters are replaced with the = padding
character, so the output length is always a multiple of four.
$ python manual_b64.py 01001101 01100001 01101110 010011 010110 000101 101110 TWFu TWE= True TQ== True TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcmsu True
Padding and size
Every three bytes of input become four characters of output.
import base64
for n in range(1, 8):
data = b'x' * n
enc = base64.b64encode(data)
print(f'{n} bytes -> {len(enc)} chars {enc.decode()}')
data = bytes(3000)
print(len(base64.b64encode(data)) / len(data))
One leftover byte produces two padding characters and two leftover bytes produce one. Base64 makes data a third larger, which matters when embedding large files in JSON or HTML.
$ python padding.py 1 bytes -> 4 chars eA== 2 bytes -> 4 chars eHg= 3 bytes -> 4 chars eHh4 4 bytes -> 8 chars eHh4eA== 5 bytes -> 8 chars eHh4eHg= 6 bytes -> 8 chars eHh4eHh4 7 bytes -> 12 chars eHh4eHh4eA== 1.3333333333333333
URL-safe Base64
The + and / characters of the standard alphabet
have special meanings in URLs and file paths. The URL-safe alphabet replaces
them with - and _.
import base64
data = bytes([0xfb, 0xff, 0xbf, 0xfe])
print(base64.b64encode(data))
print(base64.urlsafe_b64encode(data))
print(base64.urlsafe_b64encode(data, padded=False))
print(base64.b64encode(data, altchars=b'-_'))
print(base64.urlsafe_b64decode('-_-_-g'))
print(base64.urlsafe_b64decode('-_-_-g=='))
We pick bytes that produce the two problematic characters. The
altchars argument of b64encode replaces them with
any two characters.
print(base64.urlsafe_b64encode(data, padded=False))
The = character also needs escaping in URL query strings, so
URL-safe Base64 is often used without padding. Since Python 3.15, the
padded=False option omits it. The decoder no longer requires the
padding either.
$ python urlsafe.py b'+/+//g==' b'-_-__g==' b'-_-__g' b'-_-__g==' b'\xfb\xff\xbf\xfa' b'\xfb\xff\xbf\xfa'
Unpadded input
Before Python 3.15, urlsafe_b64decode required the padding,
even though the padding carries no information.
import base64
import binascii
import sys
token = 'eyJzdWIiOiJqYW5lIn0'
print(sys.version.split()[0], len(token) % 4)
try:
print(base64.urlsafe_b64decode(token))
except binascii.Error as e:
print('binascii.Error:', e)
The token has 19 characters, so one padding character is missing. We run the script with Python 3.15 and Python 3.14: the new version decodes the token, while the old one raises an error.
$ python unpadded.py
3.15.2 3
b'{"sub":"jane"}'
$ python3.14 unpadded.py
3.14.0 3
binascii.Error: Incorrect padding
Restoring padding
Code that must run on older versions adds the missing padding before decoding.
import base64
def b64url_decode(s):
return base64.urlsafe_b64decode(s + '=' * (-len(s) % 4))
def b64url_encode(data):
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
for s in ['aGk', 'aGVs', 'aGVsbG8', 'aGVsbG8gd29ybGQ']:
print(s, b64url_decode(s))
print(b64url_encode(b'hello'))
The -len(s) % 4 expression computes how many characters are
missing to the next multiple of four. Encoding without padding is a simple
rstrip. To make padding required again in Python 3.15, pass
padded=True to urlsafe_b64decode.
$ python portable_padding.py aGk b'hi' aGVs b'hel' aGVsbG8 b'hello' aGVsbG8gd29ybGQ b'hello world' aGVsbG8
JSON Web Tokens
A JSON Web Token (JWT) consists of three parts encoded with unpadded URL-safe
Base64: a header, a payload, and a signature. With the new
padded options, the standard library handles the format without
helper functions.
import base64
import hashlib
import hmac
import json
SECRET = b'my-secret-key'
def b64url(data):
return base64.urlsafe_b64encode(data, padded=False).decode()
def make_token(payload):
header = {'alg': 'HS256', 'typ': 'JWT'}
parts = [b64url(json.dumps(obj, separators=(',', ':')).encode())
for obj in (header, payload)]
signing_input = '.'.join(parts).encode()
signature = hmac.new(SECRET, signing_input, hashlib.sha256).digest()
return '.'.join(parts + [b64url(signature)])
def read_token(token):
header, payload, signature = token.split('.')
expected = hmac.new(SECRET, f'{header}.{payload}'.encode(),
hashlib.sha256).digest()
if not hmac.compare_digest(base64.urlsafe_b64decode(signature), expected):
raise ValueError('invalid signature')
return json.loads(base64.urlsafe_b64decode(payload))
token = make_token({'sub': 'jane', 'role': 'editor', 'exp': 1800000000})
print(token)
for part in token.split('.')[:2]:
print(base64.urlsafe_b64decode(part))
print(read_token(token))
We create and verify tokens signed with HMAC-SHA256 (the HS256
algorithm). The signature is computed over the first two encoded parts joined
by a dot.
for part in token.split('.')[:2]:
print(base64.urlsafe_b64decode(part))
The header and payload are only encoded, not encrypted: anyone can read them. The signature only proves that they were not changed. Real applications should use a maintained library such as PyJWT, which also checks the expiration time and the algorithm.
$ python jwt_token.py
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqYW5lIiwicm9sZSI6ImVkaXRvciIsImV4cCI6MTgwMDAwMDAwMH0.gmH-hYhb08aVXPMzVx-HeOKhUi0C5XJABg_Vw3wvksM
b'{"alg":"HS256","typ":"JWT"}'
b'{"sub":"jane","role":"editor","exp":1800000000}'
{'sub': 'jane', 'role': 'editor', 'exp': 1800000000}
Validating input
By default, b64decode silently discards characters that are not
in the alphabet.
import base64
import binascii
dirty = 'SGVs bG8s!IHdv\ncmxk'
print(base64.b64decode(dirty))
try:
base64.b64decode(dirty, validate=True)
except binascii.Error as e:
print('binascii.Error:', e)
wrapped = 'SGVsbG8s\nIHdvcmxk\n'
print(base64.b64decode(wrapped, ignorechars=b'\n'))
try:
base64.b64decode(dirty, ignorechars=b'\n')
except binascii.Error as e:
print('binascii.Error:', e)
The input contains a space, an exclamation mark, and a newline, and still decodes. This is convenient for wrapped text, but it hides corrupted data.
base64.b64decode(dirty, validate=True)
With validate=True, any character outside the alphabet raises
binascii.Error.
print(base64.b64decode(wrapped, ignorechars=b'\n'))
The ignorechars parameter, new in Python 3.15, lists characters
that are skipped; all other invalid characters are errors. Passing it turns
on validation, so we can accept wrapped lines and still reject garbage.
$ python validate.py b'Hello, world' binascii.Error: Only base64 data is allowed b'Hello, world' binascii.Error: Only base64 data is allowed
Canonical encodings
When the last group is incomplete, some bits of the last character are not used. The encoder sets them to zero, but the decoder ignores them, so several strings decode to the same bytes.
import base64
import binascii
for s in ['aGk=', 'aGl=', 'aGm=', 'aGn=']:
print(s, base64.b64decode(s))
for s in ['aGk=', 'aGl=']:
try:
print(s, base64.b64decode(s, canonical=True))
except binascii.Error as e:
print(s, 'binascii.Error:', e)
print(base64.b32decode('NBUQ===='), base64.b32decode('NBUR===='))
try:
base64.b32decode('NBUR====', canonical=True)
except binascii.Error as e:
print('binascii.Error:', e)
Four different strings decode to b'hi'. If an application
compares encoded values, for example signatures or cache keys, an attacker
can produce different strings that mean the same thing.
print(s, base64.b64decode(s, canonical=True))
The canonical=True option, new in Python 3.15, accepts only the
encoding the encoder would produce. It works for Base32 and Base85 decoders,
too.
$ python canonical.py aGk= b'hi' aGl= b'hi' aGm= b'hi' aGn= b'hi' aGk= b'hi' aGl= binascii.Error: Non-zero padding bits b'hi' b'hi' binascii.Error: Non-zero padding bits
Mixed alphabets
The URL-safe decoder has always accepted the standard + and
/ characters, too. Python 3.15 deprecates this.
import base64
import warnings
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter('always')
print(base64.urlsafe_b64decode('-_-_-g=='))
print(base64.urlsafe_b64decode('+/+/+g=='))
for w in caught:
print(f'{w.category.__name__}: {w.message}')
Both calls give the same bytes, but the second one emits a
FutureWarning. In a future version, characters from the other
alphabet will be discarded. We record the warnings and print them.
$ python mixed_alphabet.py b'\xfb\xff\xbf\xfa' b'\xfb\xff\xbf\xfa' FutureWarning: invalid character '+' in URL-safe Base64 data will be discarded in future Python versions
Line wrapping
Email (MIME) and PEM files limit the length of lines. Since Python 3.15, the
wrapcol parameter inserts a newline after the given number of
characters.
import base64 data = bytes(range(60)) print(base64.b64encode(data, wrapcol=32).decode()) print() print(base64.encodebytes(data).decode(), end='') print() print(base64.b64encode(data, wrapcol=76) + b'\n' == base64.encodebytes(data)) print(base64.b16encode(data[:24], wrapcol=16).decode())
The older encodebytes function always wraps at 76 characters and
adds a trailing newline. With wrapcol=76, b64encode
gives the same lines without the final newline. The Base16 and Base32
encoders accept wrapcol, too.
$ python wrap_lines.py AAECAwQFBgcICQoLDA0ODxAREhMUFRYX GBkaGxwdHh8gISIjJCUmJygpKissLS4v MDEyMzQ1Njc4OTo7 AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4 OTo7 True 0001020304050607 08090A0B0C0D0E0F 1011121314151617
Data URIs
A data URI embeds a file directly in an HTML or CSS document. We use a small SVG image.
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"><circle cx="10" cy="10" r="8" fill="tomato"/></svg>
The image shows a red circle.
import base64
import mimetypes
from pathlib import Path
def data_uri(path):
mime, _ = mimetypes.guess_type(path)
data = base64.b64encode(Path(path).read_bytes()).decode()
return f'data:{mime};base64,{data}'
uri = data_uri('dot.svg')
print(uri)
print(f'<img src="{uri[:40]}..." alt="dot">')
The URI consists of the MIME type, the base64 marker, and the
encoded data. We guess the MIME type from the file extension with the
mimetypes module. Data URIs save HTTP requests for small icons;
large files are better served separately, because of the 33% overhead.
$ python data_uri.py data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCI+PGNpcmNsZSBjeD0iMTAiIGN5PSIxMCIgcj0iOCIgZmlsbD0idG9tYXRvIi8+PC9zdmc+Cg== <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz..." alt="dot">
Encoding streams
The encode and decode functions work with binary
file objects and process the data in chunks.
import base64 import io import os data = os.urandom(10_000) src = io.BytesIO(data) encoded = io.BytesIO() base64.encode(src, encoded) text = encoded.getvalue() print(len(text), text.count(b'\n')) print(text[:77]) encoded.seek(0) decoded = io.BytesIO() base64.decode(encoded, decoded) print(decoded.getvalue() == data)
We use in-memory files from the io module; real files opened in
binary mode work the same way. The output is wrapped into lines of 76
characters, as with encodebytes.
$ python stream_file.py 13512 176 b'tt+FOUojupE8oMsWBkilsiLOAOw0yaFXM4Zm11occOb+2iIqVJNbIfl62NYon4tZarR8+X7gELUy\n' True
Command line
The module can be run as a script. It encodes or decodes a file, or the
standard input when no file is given. The -d option decodes.
The output of encoding is wrapped at 76 characters like
encodebytes.
$ cat msg.txt Hello there $ python -m base64 msg.txt SGVsbG8gdGhlcmUK $ python -m base64 msg.txt > msg.b64 $ python -m base64 -d msg.b64 Hello there $ echo 'UHl0aG9u' | python -m base64 -d Python
Base32 and Base16
Base32 uses 32 characters: uppercase letters and the digits 2 to 7. It is less compact than Base64, but it is case-insensitive and avoids characters that are easy to confuse, so people can read and type it. Base16 is hexadecimal.
import base64
data = b'Python'
print(base64.b32encode(data))
print(base64.b32encode(data, padded=False))
print(base64.b32hexencode(data))
print(base64.b16encode(data))
print(data.hex().upper())
print(base64.b32decode('kb4xi2dpny', casefold=True, padded=False))
print(base64.b32decode('KB4X12DPNY', map01=b'I', padded=False))
print(base64.b16decode('507974686f6e', casefold=True))
b32hexencode uses the extended hex alphabet 0-9A-V,
which keeps the sort order of the data. b16encode gives the
same result as bytes.hex in upper case.
print(base64.b32decode('kb4xi2dpny', casefold=True, padded=False))
print(base64.b32decode('KB4X12DPNY', map01=b'I', padded=False))
The decoders are strict by default. casefold=True accepts lower
case. The digits 0 and 1 are not in the alphabet; map01 maps them
to the letters O and I (or L), which users often mistype. The
padded option of the Base32 functions is new in Python 3.15.
$ python base32_16.py b'KB4XI2DPNY======' b'KB4XI2DPNY' b'A1SN8Q3FDO======' b'507974686F6E' 507974686F6E b'Python' b'Python' b'Python'
TOTP codes
Authenticator apps share their secrets as Base32 strings, usually without padding. We implement the time-based one-time password algorithm (TOTP, RFC 6238).
import base64
import hashlib
import hmac
import secrets
import struct
import time
def totp(secret_b32, at=None, digits=6, step=30, digest=hashlib.sha1):
key = base64.b32decode(secret_b32, casefold=True, padded=False)
counter = int((time.time() if at is None else at) // step)
mac = hmac.new(key, struct.pack('>Q', counter), digest).digest()
offset = mac[-1] & 0x0f
code = struct.unpack('>I', mac[offset:offset + 4])[0] & 0x7fffffff
return str(code % 10 ** digits).zfill(digits)
rfc_secret = base64.b32encode(b'12345678901234567890', padded=False).decode()
print(rfc_secret)
for t in (59, 1111111109, 1234567890, 2000000000):
print(t, totp(rfc_secret, at=t, digits=8))
new_secret = base64.b32encode(secrets.token_bytes(20), padded=False).decode()
print(new_secret, len(new_secret))
print(f'otpauth://totp/Example:jane?secret={new_secret}&issuer=Example')
print(totp(new_secret))
The secret is decoded from Base32. The current Unix time divided by 30 seconds gives a counter, which is signed with HMAC-SHA1.
offset = mac[-1] & 0x0f
code = struct.unpack('>I', mac[offset:offset + 4])[0] & 0x7fffffff
return str(code % 10 ** digits).zfill(digits)
The low four bits of the last byte select four bytes of the HMAC, which are read as a 31-bit integer. The last digits form the code.
for t in (59, 1111111109, 1234567890, 2000000000):
print(t, totp(rfc_secret, at=t, digits=8))
We check the implementation with the SHA-1 test vectors from the RFC; the codes match. Then we create a new secret with secrets and build a provisioning URI, which authenticator apps read from a QR code.
$ python totp.py GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ 59 94287082 1111111109 07081804 1234567890 89005924 2000000000 69279037 7RPR572BURUO4B5EJO7O33H5MQIRPGZ5 32 otpauth://totp/Example:jane?secret=7RPR572BURUO4B5EJO7O33H5MQIRPGZ5&issuer=Example 809672
Base85, Ascii85, and Z85
The Base85 encodings turn four bytes into five characters, so they are more compact than Base64. The module supports three alphabets.
import base64 data = b'Hello, world!' print(base64.b85encode(data)) print(base64.a85encode(data)) print(base64.a85encode(data, adobe=True)) print(base64.z85encode(data)) print(base64.z85encode(data, pad=True)) print(base64.a85encode(bytes(8))) print(base64.a85encode(b' ', foldspaces=True)) print(base64.b85decode(base64.b85encode(data)) == data)
b85encode uses the alphabet of Git binary diffs.
a85encode implements Ascii85 from PostScript and PDF; with
adobe=True, the output is framed with <~ and
~>. z85encode uses the ZeroMQ alphabet, which
is safe in source code strings.
print(base64.z85encode(data, pad=True))
Z85 requires the input length to be a multiple of four. The
pad parameter, new for z85encode in Python 3.15,
pads the input with zero bytes.
print(base64.a85encode(bytes(8))) print(base64.a85encode(b' ', foldspaces=True))
Ascii85 abbreviates a group of four zero bytes as z, and, with
foldspaces, a group of four spaces as y.
$ python base85.py b'NM&qnZ!92pZ*pv8Ap' b'87cURD_*#TDfTZ)+T' b'<~87cURD_*#TDfTZ)+T~>' b'nm=QNz.92Pz/PV8aP' b'nm=QNz.92Pz/PV8aPIGx' b'zz' b'y' True
Comparing sizes
The encodings differ in how much they enlarge the data.
import base64
data = bytes(range(256)) * 4
encoders = {
'hex (b16)': base64.b16encode,
'base32': base64.b32encode,
'base64': base64.b64encode,
'base85': base64.b85encode,
'ascii85': base64.a85encode,
'z85': lambda b: base64.z85encode(b, pad=True),
}
print(f'{len(data)} bytes of input')
for name, encode in encoders.items():
n = len(encode(data))
print(f'{name:<10} {n:>5} chars {n / len(data):.2f}x')
We encode 1024 bytes with each encoding. Hexadecimal doubles the size, Base32 adds 60%, Base64 about a third, and Base85 a quarter. Base32 includes its padding, and the Base64 overhead comes out slightly over 4/3 because 1024 is not a multiple of three.
$ python sizes.py 1024 bytes of input hex (b16) 2048 chars 2.00x base32 1640 chars 1.60x base64 1368 chars 1.34x base85 1280 chars 1.25x ascii85 1280 chars 1.25x z85 1280 chars 1.25x
Performance
In Python 3.15, the Base64 implementation in binascii was
optimized to make better use of CPU pipelining.
import base64
import os
import sys
import timeit
data = os.urandom(1_000_000)
encoded = base64.b64encode(data)
enc = min(timeit.repeat(lambda: base64.b64encode(data), number=50, repeat=5)) / 50
dec = min(timeit.repeat(lambda: base64.b64decode(encoded), number=50, repeat=5)) / 50
print(sys.version.split()[0])
print(f'encode: {len(data) / enc / 1e9:.2f} GB/s')
print(f'decode: {len(encoded) / dec / 1e9:.2f} GB/s')
We measure the throughput of encoding and decoding one megabyte of random data and run the script with Python 3.15 and Python 3.14 on the same machine. The script prints the version it runs on. The exact numbers depend on the CPU.
$ python bench.py 3.15.2 encode: 2.12 GB/s decode: 2.10 GB/s $ python3.14 bench.py 3.14.0 encode: 0.61 GB/s decode: 0.79 GB/s
Base64 is not encryption
Base64 hides data from casual eyes, but it provides no security.
import base64
credentials = base64.b64encode(b'jane:s3cret').decode()
print(f'Authorization: Basic {credentials}')
print(base64.b64decode(credentials))
HTTP Basic authentication sends the user name and password encoded in Base64. Anyone who sees the header can decode it, which is why Basic authentication must be used only over HTTPS. To protect data, use encryption; to detect changes, use a signature or an HMAC.
$ python not_encryption.py Authorization: Basic amFuZTpzM2NyZXQ= b'jane:s3cret'
Source
base64 – Base16, Base32, Base64, Base85 Data Encodings
RFC 4648 – The Base16, Base32, and Base64 Data Encodings
In this article we have encoded and decoded binary data with the Python
base64 module.
Author
List all Python tutorials.