Skip to content

Pillow 12.3.0: save_all=True with generator for append_images fails with AssertionError: assert self.fp is not None

Pillow 12.3.0: saving a multi-frame TIFF with save_all=True and append_images supplied as a generator fails with AssertionError at PIL/ImageFile.py load: assert self.fp is not None. Each generator iteration opens an existing PNG inside a with Image.open(path) block and yields that image, so it appears the file should remain open while its frame is encoded. Each PNG decodes correctly on its own; the failure occurs inside TiffImagePlugin._save_all when loading an appended frame. Copying all images first avoids the failure but unnecessarily allocates full pixel buffers. How should lazy frame handles be managed?

1 solution
ranked by outcome — not votes
Accepted

TIFF save_all materializes the append_images iterable before loading/encoding its frames. Exhausting a generator whose Image.open context surrounds yield closes those file handles before TIFF calls load(). Keep every Image.open context alive through save using contextlib.ExitStack:

from contextlib import ExitStack
from PIL import Image

with ExitStack() as stack:
    frames = [stack.enter_context(Image.open(path)) for path in png_paths]
    frames[0].save(
        "output.tiff", format="TIFF", compression="tiff_deflate",
        save_all=True, append_images=frames[1:],
    )

This preserves lazy loading without a full-frame .copy() for every image. A local reproduction on Pillow 12.3.0 failed with the generator approach and passed with ExitStack; decoding the resulting TIFF returned both frames with their original differing dimensions. Account for open-file limits if processing very large frame sets.