Image.open(io.BytesIO(data)) where data is the bytes. For a URL:
Image.open(io.BytesIO(requests.get(url, timeout=30).content)), after checking the response status. For base64:
Image.open(io.BytesIO(base64.b64decode(s))).
From bytes
import io
from PIL import Image
data = open("photo.jpg", "rb").read() # bytes from anywhere: a database, a socket, an API...
img = Image.open(io.BytesIO(data))
print(type(data).__name__, len(data), "->", img.format, img.size)
bytes 239040 -> JPEG (1600, 1067)
You do not need to call seek(0) first; current Pillow rewinds file objects itself
(we checked).
From a URL
With the requests library (these two snippets need network access, so they are the only ones on this page we did not run):
import io
import requests
from PIL import Image
url = "https://images-assets.nasa.gov/image/Artemis%20II%20at%20the%20pad%20sunset%2001282026_1/Artemis%20II%20at%20the%20pad%20sunset%2001282026_1~small.jpg"
resp = requests.get(url, timeout=30)
resp.raise_for_status() # fail loudly on 403/404 instead of later
img = Image.open(io.BytesIO(resp.content))
print(img.format, img.size)
Always check the status before handing the body to Pillow. A 403 or 404 page is HTML, and Pillow reports it as
cannot identify image file, which hides the real problem.
Some sites also reject requests without a User-Agent header. With only the standard library:
import io
from urllib.request import Request, urlopen
from PIL import Image
req = Request("https://example.com/image.jpg", headers={"User-Agent": "my-app/1.0"})
with urlopen(req, timeout=30) as resp: # standard library only, no requests needed
img = Image.open(io.BytesIO(resp.read()))
From a base64 string
import base64
import io
from PIL import Image
# A base64 string as you might receive it in JSON from an API or a browser
b64 = base64.b64encode(open("logo.png", "rb").read()).decode("ascii")
print("payload starts:", b64[:24], "...", len(b64), "characters")
img = Image.open(io.BytesIO(base64.b64decode(b64)))
print(img.format, img.mode, img.size)
payload starts: iVBORw0KGgoAAAANSUhEUgAA ... 5256 characters
PNG RGBA (600, 360)
From a data URI (data:image/png;base64,...)
Browsers send canvases and file previews as data URIs. Strip the prefix before decoding:
import base64
import io
import re
from PIL import Image
data_uri = "data:image/png;base64," + base64.b64encode(open("logo.png", "rb").read()).decode()
def open_data_uri(uri):
match = re.match(r"data:image/[\w.+-]+;base64,(.*)", uri, re.DOTALL)
if not match:
raise ValueError("not a base64 image data URI")
return Image.open(io.BytesIO(base64.b64decode(match.group(1))))
print(open_data_uri(data_uri).size)
(600, 360)
If the string was cut off or mangled on the way, decoding fails before Pillow is even involved:
import base64
import binascii
b64 = base64.b64encode(open("logo.png", "rb").read()).decode()
broken = b64.rstrip("=")[:-1] # e.g. truncated by a form field or a log line
try:
base64.b64decode(broken, validate=True)
except binascii.Error as e:
print("binascii.Error:", e)
binascii.Error: Invalid base64-encoded string: number of data characters (5253) cannot be 1 more than a multiple of 4
From a stream that cannot seek
Raw HTTP responses and some pipes can only be read forwards. Pillow detects this and reads the whole stream into memory, so you can pass them directly:
import io
from PIL import Image
class SlowStream(io.RawIOBase):
"""Stands in for a network stream that cannot seek (like a raw HTTP response)."""
def __init__(self, data):
self._buf = io.BytesIO(data)
def readable(self):
return True
def readinto(self, b):
chunk = self._buf.read(len(b))
b[:len(chunk)] = chunk
return len(chunk)
stream = SlowStream(open("photo.jpg", "rb").read())
print("seekable:", stream.seekable())
img = Image.open(stream) # Pillow reads it into memory when it cannot seek
print(img.format, img.size)
seekable: False
JPEG (1600, 1067)
Accepting images from users safely
For uploads and URLs supplied by users, limit the file size and the pixel count before decoding. Because the header is read first, you can reject a huge image without allocating memory for it:
import io
from PIL import Image
MAX_BYTES = 10 * 1024 * 1024 # refuse files over 10 MB
MAX_PIXELS = 40_000_000 # and images over 40 megapixels
def safe_open(data):
if len(data) > MAX_BYTES:
raise ValueError("file too large")
img = Image.open(io.BytesIO(data))
if img.width * img.height > MAX_PIXELS: # checked from the header, before decoding
raise ValueError(f"image too large: {img.size}")
img.load()
return img
print(safe_open(open("photo.jpg", "rb").read()).size)
(1600, 1067)
Pillow has its own guard too: images over about 89 million pixels trigger a
DecompressionBombWarning, and over about 179 million an error. To restrict which formats are accepted, pass
formats=["JPEG", "PNG", "WEBP"] to Image.open().
Going the other way, from an image to bytes or base64, is covered in converting an image to bytes and base64.
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.