buf = io.BytesIO(); img.save(buf, format="PNG"); data = buf.getvalue(). For base64:
base64.b64encode(data).decode(), and for a data URI prefix it with data:image/png;base64,.
Image to bytes
import io
from PIL import Image
img = Image.open("photo.jpg").resize((800, 534))
buf = io.BytesIO()
img.save(buf, format="PNG") # with a buffer there is no file name, so give the format
png_bytes = buf.getvalue()
print(len(png_bytes), "bytes, starts with", png_bytes[:8])
436256 bytes, starts with b'\x89PNG\r\n\x1a\n'
Those first bytes are the PNG signature, proof that you have a real PNG file in memory. Use format="JPEG" or
"WEBP" with a quality for smaller results (JPEG needs an RGB or L image; see
cannot write mode as JPEG).
tobytes() is not what you want (usually)
img.tobytes() returns the raw, uncompressed pixel values with no header. It is much bigger, and nothing else
can open it as an image:
import io
from PIL import Image
img = Image.open("photo.jpg").resize((800, 534))
raw = img.tobytes() # raw pixel values, no file format
buf = io.BytesIO(); img.save(buf, "JPEG", quality=85)
print("tobytes(): ", len(raw), "bytes =", img.width, "x", img.height, "x", len(img.getbands()))
print("saved JPEG: ", buf.tell(), "bytes")
try:
Image.open(io.BytesIO(raw)) # raw pixels are not a file Pillow can open
except Exception as e:
print(type(e).__name__ + ":", e)
same = Image.frombytes(img.mode, img.size, raw) # you need mode and size to rebuild it
print("frombytes():", same.size, same.mode)
tobytes(): 1281600 bytes = 800 x 534 x 3
saved JPEG: 52193 bytes
UnidentifiedImageError: cannot identify image file <_io.BytesIO object at 0x7fc90ef3ade0>
frombytes(): (800, 534) RGB
Use tobytes()/frombytes() only to pass pixels between libraries in the same process, where both
sides know the mode and size. For storing or sending images, save to a format.
Base64 and data URIs
import base64
import io
from PIL import Image
img = Image.open("logo.png").resize((120, 72))
buf = io.BytesIO()
img.save(buf, format="PNG")
b64 = base64.b64encode(buf.getvalue()).decode("ascii") # str, ready for JSON
data_uri = f"data:image/png;base64,{b64}"
print(len(b64), "chars;", data_uri[:40] + "...")
open("inline.html", "w").write(f'<img src="{data_uri}" alt="logo">')
3968 chars; data:image/png;base64,iVBORw0KGgoAAAANSU...
Base64 makes data about a third bigger. We measured it for three formats:
import base64
import io
from PIL import Image
img = Image.open("photo.jpg").resize((800, 534))
for fmt, opts in [("PNG", {}), ("JPEG", {"quality": 85}), ("WEBP", {"quality": 80})]:
buf = io.BytesIO()
img.save(buf, fmt, **opts)
b64 = base64.b64encode(buf.getvalue())
print(f"{fmt:<5} {buf.tell() / 1024:7.1f} KB binary -> {len(b64) / 1024:7.1f} KB base64 (+{len(b64) / buf.tell() - 1:.0%})")
PNG 426.0 KB binary -> 568.0 KB base64 (+33%)
JPEG 51.0 KB binary -> 68.0 KB base64 (+33%)
WEBP 27.7 KB binary -> 36.9 KB base64 (+33%)
So embed small images (icons, thumbnails) as data URIs, and serve larger ones as files.
Return an image from a web app without saving a file
These framework snippets are standard patterns (they need a running server, so we did not execute them here):
Flask
import io
from flask import Flask, send_file
from PIL import Image, ImageDraw
app = Flask(__name__)
@app.get("/badge/<text>.png")
def badge(text):
img = Image.new("RGB", (240, 60), "#2563eb")
ImageDraw.Draw(img).text((120, 30), text, fill="white", anchor="mm", font_size=28)
buf = io.BytesIO()
img.save(buf, "PNG")
buf.seek(0) # send_file reads from the current position
return send_file(buf, mimetype="image/png")
The seek(0) matters for Flask: send_file() reads from the current position, which is the end of the
buffer after saving. (It is Image.open() that rewinds automatically, not other libraries.)
FastAPI
import io
from fastapi import FastAPI
from fastapi.responses import Response
from PIL import Image
app = FastAPI()
@app.get("/thumb.webp")
def thumb():
img = Image.open("photo.jpg")
img.thumbnail((400, 400))
buf = io.BytesIO()
img.save(buf, "WEBP", quality=80)
return Response(content=buf.getvalue(), media_type="image/webp")
Django
import io
from django.core.files.base import ContentFile
from PIL import Image
def make_thumbnail(instance):
img = Image.open(instance.photo) # an ImageField / uploaded file
img.thumbnail((400, 400))
buf = io.BytesIO()
img.convert("RGB").save(buf, "JPEG", quality=85)
instance.thumbnail.save("thumb.jpg", ContentFile(buf.getvalue()), save=False)
If Django says it "cannot use ImageField because Pillow is not installed", see No module named 'PIL'; it is the same environment problem.
Related
The outputs and images on this page are the real results of running every example with Pillow 12.3.0 on Python 3.13 (Linux) on 2026-10-15. Every example is also re-run automatically on macOS (Darwin 25.6.0, Pillow 12.3.0), Linux (Linux 6.17.0-1022-azure, Pillow 12.3.0), Windows (Windows 2025Server, Pillow 12.3.0); the line under each example shows the result per operating system. The DejaVu Sans font files sit next to each script so font examples behave the same everywhere. Sample photos: NASA/Sam Lott, public domain. Spotted something wrong? Tell us.