Quick fix

Convert the array to uint8 in the 0–255 range before calling Image.fromarray():

Image.fromarray((np.clip(arr, 0, 1) * 255).astype(np.uint8)) for floats in 0..1, or Image.fromarray(arr.astype(np.uint8)) for integers that are already 0..255.

What the error looks like

The two versions we see most: a colour image stored as floats, and an integer mask built with NumPy's default int type.

from_float.py
import numpy as np
from PIL import Image

rgb = np.random.default_rng(0).random((64, 64, 3), dtype=np.float32)   # float32, values 0..1
Image.fromarray(rgb)
Tested on ✓ Linux ✓ Windows ✓ macOS
Traceback (Pillow 12.3.0)
TypeError: Cannot handle this data type: (1, 1, 3), <f4
from_int.py
import numpy as np
from PIL import Image

mask = np.zeros((64, 64), dtype=int)       # int64 on Linux and macOS
mask[16:48, 16:48] = 255
Image.fromarray(mask)
Tested on ✓ Linux ✓ Windows ✓ macOS
Traceback (Pillow 12.3.0)
TypeError: Cannot handle this data type: (1, 1), <i8

How to read the message

The tuple and code after "data type" are what Pillow looked up in its table of supported array layouts:

what_pillow_sees.py
import numpy as np

for arr in [np.zeros((2, 2, 3), np.float32), np.zeros((2, 2), np.int64), np.zeros((3, 2, 2), np.uint8)]:
    # This is the (shape-suffix, typestr) pair Pillow looks up:
    print(arr.dtype, arr.shape, "->", (1, 1) + arr.shape[2:], arr.__array_interface__["typestr"])
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
float32 (2, 2, 3) -> (1, 1, 3) <f4
int64 (2, 2) -> (1, 1) <i8
uint8 (3, 2, 2) -> (1, 1, 2) |u1

Supported combinations include u1 with 1, 2, 3 or 4 channels (L, LA, RGB, RGBA), 2-D b1 (mode 1), 2-D u2 (I;16), 2-D i4 (I) and 2-D f4/f8 (F). Colour floats and 64-bit integers are not. The full table is on the NumPy guide.

Fix 1: float arrays

Scale to 0–255, clip, and convert. Clipping matters: values slightly outside 0..1 (common after augmentation or model output) would otherwise wrap around:

from_float.py
import numpy as np
from PIL import Image

rgb = np.random.default_rng(0).random((64, 64, 3), dtype=np.float32)   # 0..1
img = Image.fromarray((np.clip(rgb, 0, 1) * 255).round().astype(np.uint8))
print(img.mode, img.size)
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
RGB (64, 64)

If the floats are already in 0..255, skip the * 255.

Fix 2: int64 (or other wide integers)

from_int.py
import numpy as np
from PIL import Image

mask = np.zeros((64, 64), dtype=int)
mask[16:48, 16:48] = 255
img = Image.fromarray(mask.astype(np.uint8))      # values already 0..255
print(img.mode, img.getextrema())
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
L (0, 255)

If the values can exceed 255, decide what they mean first: clip with np.clip(arr, 0, 255), or rescale with arr * 255 // arr.max(). For 16-bit data such as depth maps or medical images, arr.astype(np.uint16) gives a 16-bit I;16 image you can save as PNG or TIFF without losing precision.

Fix 3: channel-first arrays (PyTorch, some ML libraries)

Arrays shaped (3, height, width) fail for a different reason: Pillow reads the last axis as channels.

channel_first.py
import numpy as np
from PIL import Image

# PyTorch tensors and some libraries store images channel-first: (channels, height, width)
chw = np.zeros((3, 224, 224), dtype=np.uint8)
try:
    Image.fromarray(chw)
except TypeError as e:
    print("channel-first:", e)

hwc = np.transpose(chw, (1, 2, 0))               # -> (height, width, channels)
print("transposed:", Image.fromarray(hwc).mode, Image.fromarray(hwc).size)
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
channel-first: Cannot handle this data type: (1, 1, 224), |u1
transposed: RGB (224, 224)

With a PyTorch tensor, torchvision.transforms.functional.to_pil_image(tensor) handles the transpose and scaling for you.

A helper that handles all of these

to_image.py
import numpy as np
from PIL import Image


def to_image(arr):
    """Convert common array layouts (float 0..1, any int dtype, CHW, single-channel 3-D) to a PIL image."""
    arr = np.asarray(arr)
    if arr.ndim == 3 and arr.shape[0] in (1, 3, 4) and arr.shape[-1] not in (1, 3, 4):
        arr = np.transpose(arr, (1, 2, 0))           # CHW -> HWC
    if arr.ndim == 3 and arr.shape[-1] == 1:
        arr = arr[..., 0]                           # (h, w, 1) -> (h, w)
    if arr.dtype.kind == "f":
        scale = 255 if arr.max() <= 1.0 else 1      # treat floats as 0..1 if they look like it
        arr = np.clip(arr * scale, 0, 255).round()
    return Image.fromarray(arr.astype(np.uint8))


for a in [np.random.rand(32, 32, 3), np.ones((3, 32, 32), np.float32), np.full((32, 32), 300), np.zeros((32, 32, 1), np.int64)]:
    print(a.dtype, a.shape, "->", to_image(a).mode)
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
float64 (32, 32, 3) -> RGB
float32 (3, 32, 32) -> RGB
int64 (32, 32) -> L
int64 (32, 32, 1) -> L

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.