Python base64: Encode Binary Data

Published on: August 18, 2026
Reading time: 5 minutes
A person in a hoodie coding on dual monitors, depicting cybersecurity and hacking themes.

The Python base64 module converts binary bytes into printable ASCII and decodes those representations back to bytes. This allows images, identifiers, keys, and small blobs to travel through text-oriented formats such as JSON, configuration files, headers, and some URL components.

Base64 is an encoding, not encryption. Anyone who can read the string can recover the original bytes. It does not authenticate the sender or reliably protect integrity. Use encryption for confidentiality and authenticated hashes or signatures for authenticity.

Basic encoding and decoding

import base64

data = b"binary message\x00\xff"
encoded = base64.b64encode(data)
restored = base64.b64decode(encoded)

assert restored == data
print(encoded)

Encoding functions accept bytes-like objects and return ASCII bytes. When storing the result in JSON, convert at the boundary:

text = base64.b64encode(data).decode("ascii")
original = base64.b64decode(text)

Base64 increases size

Three source bytes become four text characters, creating roughly 33 percent overhead before surrounding JSON or protocol syntax. Base64 is not compression. If the transport requires text and size matters, compress appropriate data first with a format such as Python zlib, then encode the compressed bytes.

Strict decoding with validate

By default, b64decode() discards characters outside the alphabet before checking padding. This can accommodate wrapped MIME text, but it may accept unexpected garbage. For API input and tokens, enable strict validation:

import base64
import binascii

try:
    data = base64.b64decode(value, validate=True)
except (binascii.Error, ValueError) as exc:
    raise ValueError("invalid Base64") from exc

Also limit input and output lengths. A very large encoded string can exhaust memory before its decoded contents are inspected.

Padding

The = character completes the final four-character group. Some URL-oriented protocols remove padding. Restore only what is necessary and document the rule:

import base64

def decode_unpadded(text: str) -> bytes:
    if len(text) > 10_000:
        raise ValueError("input is too long")
    padding = "=" * (-len(text) % 4)
    return base64.urlsafe_b64decode(text + padding)

Do not add arbitrary padding without validating the alphabet and allowed length. A protocol should state whether padding is required, optional, or forbidden.

URL-safe Base64

urlsafe_b64encode() substitutes - for + and _ for /:

import base64

identifier = base64.urlsafe_b64encode(b"file/2026+version")
print(identifier)
print(base64.urlsafe_b64decode(identifier))

The output may still contain =. URL-safe describes the alphabet, not automatic suitability for every path or query context. Build complete URLs with Python urllib.parse.

Alternative alphabets

b64encode() accepts exactly two replacement bytes for + and /:

import base64

custom = base64.b64encode(b"data", altchars=b"-_")
original = base64.b64decode(custom, altchars=b"-_", validate=True)

Use the dedicated URL-safe helpers for the standard URL alphabet. Custom alphabets reduce interoperability and must be documented on both sides.

Binary files in JSON

import base64
import json
from pathlib import Path

path = Path("icon.png")
content = path.read_bytes()

if len(content) > 2 * 1024 * 1024:
    raise ValueError("file exceeds the limit")

payload = json.dumps({
    "name": path.name,
    "content_base64": base64.b64encode(content).decode("ascii"),
})

For large files, prefer binary upload, multipart requests, or object storage. Base64 increases bandwidth and often keeps original bytes, encoded text, and decoded bytes in memory simultaneously.

Data URLs

import base64

data = b"..."
media_type = "image/png"
data_url = f"data:{media_type};base64,{base64.b64encode(data).decode('ascii')}"

Do not trust the declared media type. Validate the actual format and apply an allowlist. Data URLs can carry HTML, SVG, or scripts and may create XSS risks when inserted into a page without a restrictive policy.

Base32

Base32 uses a smaller, often more human-friendly alphabet:

import base64

code = base64.b32encode(b"temporary secret")
print(code)
print(base64.b32decode(code))

b32decode() rejects lowercase by default. casefold=True accepts it. The map01 option can map visually similar digits, but the secure default is to reject those substitutions and avoid ambiguity.

Base32 Hex

b32hexencode() and b32hexdecode() use the extended hexadecimal alphabet from RFC 4648. Digits 0 and 1 are real alphabet members and are not interchangeable with letters.

Base16

import base64

hex_value = base64.b16encode(b"ABC")
print(hex_value)
print(base64.b16decode(hex_value))

Decoding accepts uppercase by default. Enable casefold only if the protocol allows lowercase. For ordinary hexadecimal identifiers, bytes.hex() and bytes.fromhex() may be more direct.

Ascii85, Base85, and Z85

Base85 families represent four bytes with five characters and have less overhead than Base64. Python exposes several incompatible variants:

  • a85encode(): Ascii85 for PostScript and PDF-related formats.
  • b85encode(): the alphabet used by tools such as Git.
  • z85encode(): ZeroMQ Z85, available since Python 3.13.
import base64

data = b"12345678"
print(base64.a85encode(data))
print(base64.b85encode(data))
print(base64.z85encode(data))

They are not interchangeable. Follow the external specification for markers, padding, whitespace, and alphabet.

Z85 length rules

Z85 requires input length to be a multiple of four and encoded length to be a multiple of five. If an application adds padding, it must also preserve the original length or define an unambiguous removal rule.

Legacy MIME interface

encodebytes() inserts line breaks every 76 characters according to MIME. For complete email messages, use the email package, which manages headers, transfer encodings, and multipart structure. The modern functions are better for APIs and application storage.

HTTP Basic Authentication

Basic Authentication encodes username:password with Base64. The encoding does not protect the credentials; TLS provides transport confidentiality.

import base64

credentials = "user:password".encode("utf-8")
header = "Basic " + base64.b64encode(credentials).decode("ascii")

Never log the header, avoid long-lived shared passwords, and prefer short-lived credentials or stronger mechanisms when possible.

Never store passwords as Base64

Base64-encoded passwords are equivalent to plaintext. Password storage requires a password-hashing algorithm with a salt and configurable cost. Private keys and tokens belong in encrypted storage or a secret manager.

Signed tokens

JWT and similar formats use Base64URL to represent sections, but their security comes from a signature or MAC. Decoding a token does not verify it. A complete verifier must enforce the algorithm, key, signature, issuer, audience, expiry, and other claims.

Bound decoding

import base64
import binascii

MAX_TEXT = 4 * 1024 * 1024
MAX_OUTPUT = 3 * 1024 * 1024

def decode_limited(text: str) -> bytes:
    if len(text) > MAX_TEXT:
        raise ValueError("Base64 input exceeds the limit")
    try:
        data = base64.b64decode(text, validate=True)
    except binascii.Error as exc:
        raise ValueError("invalid Base64") from exc
    if len(data) > MAX_OUTPUT:
        raise ValueError("decoded output exceeds the limit")
    return data

The encoded-to-decoded ratio helps estimate a limit, but the decoded object must still be validated according to its claimed type and structure.

Constant-time comparison

When decoded bytes represent an expected MAC or signature value, compare with hmac.compare_digest() after validating length and format. This reduces timing differences from an ordinary equality comparison.

Structured binary payloads

Base64 transports bytes but does not define their layout. Use Python struct for a versioned binary header with magic bytes and bounded lengths. Batch decoding can use Python queue to apply backpressure.

Testing strategy

Test empty input, every length modulo four, missing and excessive padding, invalid characters, non-ASCII text, standard and URL-safe alphabets, input limits, ambiguous Base32 characters, Base85 padding, random-data round trips, and RFC 4648 test vectors.

Best practices

  • Keep bytes internally.
  • Convert to ASCII only at a boundary.
  • Use validate=True for external input.
  • Set limits before and after decoding.
  • Select the alphabet from a specification.
  • Never confuse encoding with security.
  • Do not log credentials or tokens.
  • Validate decoded binary content.

Conclusion

Python base64 provides Base16, Base32, Base64, Ascii85, Base85, and Z85 for transporting bytes through text channels. The API is simple, but reliable systems need strict validation, explicit limits, and a clear protocol.

Read the official base64 documentation and RFC 4648. Encoding solves representation; confidentiality and authenticity require dedicated security tools.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Developer configuring a Python HTTPS server and TLS certificate
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    HTTPSServer: Build a Local HTTPS Server in Python

    Learn Python HTTPSServer for local HTTPS services, TLS certificates, threaded handling, testing, and practical security limits.

    Ler mais

    Tempo de leitura: 4 minutos
    08/10/2026
    Protected files representing secure TAR extraction with Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    tarfile extraction_filter: Extract TAR Safely

    Learn Python tarfile extraction_filter for safer TAR extraction, path validation, links, permissions, and resource limits.

    Ler mais

    Tempo de leitura: 5 minutos
    08/10/2026
    Python code showing warnings controlled with catch_warnings
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    catch_warnings: Capture Python Warnings in Tests

    Learn Python catch_warnings to capture, test, and control warnings with focused filters and safe temporary scopes.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Python code representing persistent pickle references
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    pickle persistent_id: Serialize External References

    Learn Python pickle persistent_id for stable external references, validation, security, performance, and long-term compatibility.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Binary code representing Python buffers and memory views
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    memoryview.count: Count Values Without Buffer Copies

    Learn Python memoryview.count to count bytes and values in buffers without copies, with formats, limits, and practical safety.

    Ler mais

    Tempo de leitura: 5 minutos
    06/10/2026
    Python code and type annotations
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    annotationlib: Avoid Circular Imports in Annotations

    Learn Python annotationlib to inspect deferred annotations, avoid circular imports, and build safer runtime tooling.

    Ler mais

    Tempo de leitura: 5 minutos
    06/10/2026