Short answer

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

from_bytes.py
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)
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
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):

from_url.py
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:

from_url_stdlib.py
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

from_base64.py
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)
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
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:

from_data_uri.py
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)
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
(600, 360)

If the string was cut off or mangled on the way, decoding fails before Pillow is even involved:

bad_base64.py
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)
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
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:

non_seekable.py
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)
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
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:

safe_open.py
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)
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
(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.

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.