Send Base64 Images
Sending the Image Itself
A base64 image source embeds the image data directly in the request. Nothing needs to be hosted or uploaded first, which makes it the most direct option for one-off images, and the right choice for images that users upload to your application:
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": "<base64 text>",
},
}
Base64 is an encoding, not compression. It represents binary data as text using 64 printable characters. Every three bytes become four characters, so encoded data is about one third larger than the original. That overhead is why the size limit applies to the encoded text (Lesson 111).
Building a Safe Image Block
Several checks belong in one function that turns a file into an image block:
- Read the bytes.
- Detect the format from its signature (Lesson 111), and reject anything unsupported.
- Check the encoded size against the limit, before sending.
- Build the block with the detected media type, not the file extension.
import base64
from media_utils import MAX_IMAGE_BASE64_BYTES, detect_media_type, encode_base64
def image_block(data: bytes) -> dict:
media_type = detect_media_type(data)
if media_type is None:
raise ValueError("unsupported image format; use JPEG, PNG, GIF, or WebP")
encoded = encode_base64(data)
if len(encoded) > MAX_IMAGE_BASE64_BYTES:
raise ValueError(f"image is too large after encoding ({len(encoded):,} characters); "
"resize it to under 10 MB encoded")
return {"type": "image", "source": {"type": "base64", "media_type": media_type, "data": encoded}}
Because the function works on bytes, it fits both files from disk and uploads received by a web application.
Resizing Large Images
When an image is too large, resize it before encoding. The Pillow library does this in a few lines:
python -m pip install pillow
from io import BytesIO
from PIL import Image
def shrink(data: bytes, longest_edge: int = 1568) -> bytes:
image = Image.open(BytesIO(data))
image.thumbnail((longest_edge, longest_edge)) # Keeps proportions; only shrinks
output = BytesIO()
image.convert("RGB").save(output, format="JPEG", quality=90)
return output.getvalue()
thumbnail()resizes in place while keeping the aspect ratio, and never enlarges.convert("RGB")removes transparency, because JPEG does not support it.quality=90keeps reasonable detail at a smaller size.
Use 1568 pixels for standard models, as the documentation recommends, and larger only when fine detail matters. Resizing is lossy, so keep the original if you need it later. Note that the output format changes to JPEG, so the media type passed to image_block() must change with it.
Practical Example: Checking and Sending an Uploaded Image
This example validates an image's bytes, builds the block, and asks Claude a question. An offline check shows the rejection paths, which do not need network access.
"""base64_image.py: validate image bytes, then send them as a base64 image block."""
from anthropic import Anthropic
from media_utils import MAX_IMAGE_BASE64_BYTES, detect_media_type, encode_base64, make_png
client = Anthropic()
def image_block(data: bytes) -> dict:
media_type = detect_media_type(data)
if media_type is None:
raise ValueError("unsupported image format; use JPEG, PNG, GIF, or WebP")
encoded = encode_base64(data)
if len(encoded) > MAX_IMAGE_BASE64_BYTES:
raise ValueError(f"image is too large after encoding ({len(encoded):,} characters)")
return {"type": "image", "source": {"type": "base64", "media_type": media_type, "data": encoded}}
def ask_about_image(data: bytes, question: str) -> str:
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": [image_block(data), {"type": "text", "text": question}]}],
)
return "".join(block.text for block in message.content if block.type == "text").strip()
if __name__ == "__main__":
# Offline checks: no API calls
for label, data in [
("valid PNG", make_png(32, 32, (0, 128, 255))),
("text file", b"Hello, this is not an image."),
("oversized", b"\x89PNG\r\n\x1a\n" + b"0" * (8 * 1024 * 1024)),
]:
try:
block = image_block(data)
print(f"{label}: accepted as {block['source']['media_type']}")
except ValueError as error:
print(f"{label}: rejected ({error})")
print("Claude:", ask_about_image(make_png(32, 32, (0, 128, 255)), "What color is this? One word."))
Expected output:
valid PNG: accepted as image/png
text file: rejected (unsupported image format; use JPEG, PNG, GIF, or WebP)
oversized: rejected (image is too large after encoding (11,184,824 characters))
Claude: Blue
How It Works
image_block()validates before sending. The text file fails format detection. The oversized file passes the format check, because its header is a valid PNG signature, and fails the size check instead. Each failure has its own message, so the caller can explain the problem.- The size is checked after encoding, because that is the value the API limit applies to. The oversized example is 8 MB of data plus an 8-byte header, which encodes to 11,184,824 characters. Base64 always produces four characters for every three bytes, rounded up, so that number follows directly from the data length.
- The media type comes from the bytes. The block always matches what the data actually is.
ask_about_image()builds the request using the validated block, followed by the question.- The offline checks run first, so the rejection logic can be verified without spending tokens.
Note: a valid PNG signature does not mean the rest of the file is valid. A corrupt image can pass these checks and still fail at the API. Lesson 120 covers how to handle that case.
Common Mistakes
Checking the Original Size Instead of the Encoded Size
What happens: files just under the raw limit are rejected by the API.
Fix: measure the length of the encoded string.
Trusting the File Extension
What happens: a file named .jpg that is really a PDF is sent with the wrong media type.
Fix: detect the format from the bytes.
Sending Unnecessarily Large Images
What happens: slower requests and higher token costs, with no benefit for most tasks.
Fix: resize to 1568 pixels or less unless fine detail matters.
Keeping the Only Copy After Resizing
What happens: the original high-resolution image is lost.
Fix: store the original, and send the resized copy.
Logging Image Data
What happens: base64 strings in logs are very long, and expose the image content.
Fix: log the media type and size, not the data.
Best Practices
- Validate bytes: detect the format and check the size before sending.
- Measure the encoded size, since that is what the limit applies to.
- Resize before encoding when the task allows.
- Keep originals separately from processed copies.
- Never log image data.
- Reuse validated blocks when the same image is needed twice, or use the Files API (Lesson 116).
Exercise
- Use
make_png()to create images of increasing size, such as 500 × 500, 2000 × 2000, and 4000 × 4000 pixels. Which ones are accepted, and why might a PNG of solid color not reach the limit? - Modify
shrink()to return a PNG instead of a JPEG, and explain what changes inimage_block(). - Add a check that rejects images with a width or height over 8000 pixels, using
Image.open()to read the dimensions without decoding everything. - Explain why
shrink()converts the image to RGB before saving as JPEG.
Step 2: a PNG output keeps transparency, but the output file is usually larger for photographs. image_block() does not need changes, because it detects the format from the bytes, not from the function that produced them. The media type will be image/png automatically. That is the benefit of detecting from the bytes rather than trusting a name.
Step 4: JPEG has no transparency channel. An image with transparency, or in a mode such as palette-based PNG, cannot be saved as JPEG directly, and attempting to do so raises an error or produces wrong colors. Converting to RGB first makes the output valid, and removes any transparency by flattening it onto black, which is acceptable for analysis. For graphics that depend on transparency, save as PNG instead.