Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The right way to save an image in Python depends on the object you have. Use Pillow’s Image.save() for an existing or processed image, Matplotlib’s fig.savefig() for a chart or figure, and OpenCV’s cv2.imwrite() for a NumPy-style image array. Save to a path when you need a file, or to a binary stream such as BytesIO when you need image bytes without creating a named disk file.

Choose the save method by image type

These APIs solve related but different problems. Selecting the one that matches your object avoids conversion errors and makes format options predictable.

Object you have Primary API Typical destination Important detail
Pillow Image image.save() Filename or binary file-like object The filename extension normally selects the writer; pass format= when it cannot.
Matplotlib chart or figure fig.savefig() or plt.savefig() Filename or file-like object Supports options such as DPI, bounding box, transparency, and format.
OpenCV image array cv2.imwrite() Filename The filename extension selects the encoder; check the Boolean return value.
Image bytes in memory Pillow save() with BytesIO Memory buffer Open the buffer in binary mode and specify the output format.

Save an existing or processed image with Pillow

Install Pillow

Install the package in the environment that will run your script:

python -m pip install Pillow

Pillow’s documented operation is the save() method on an Image object.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Save using the filename extension

from PIL import Image

with Image.open("input.jpg") as image:
    image.save("output.png")

Here Pillow reads input.jpg, keeps the image available inside the context manager, and writes a PNG because the destination ends in .png. The context manager closes the input file reliably after saving.

Use an explicit format for an unusual extension

from PIL import Image

with Image.open("input.jpg") as image:
    image.save("output.data", format="PNG")

An extension such as .data does not identify an image encoder. Supplying format="PNG" removes that ambiguity. Keep the extension and format aligned when other programs will consume the file.

Convert mode before saving JPEG

JPEG does not represent an alpha channel. If a source image is in RGBA or another mode that the JPEG writer cannot store, convert it first:

from PIL import Image

with Image.open("logo.png") as image:
    rgb_image = image.convert("RGB")
    rgb_image.save("logo.jpg", quality=90)

The quality argument is a format-specific Pillow option. Use options only when they apply to the chosen writer; PNG, JPEG, and other formats expose different controls.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Save an image without creating a named file

Pillow can write to any binary file-like object that supports seek(), tell(), and write(). io.BytesIO keeps the encoded result in memory.

from io import BytesIO
from PIL import Image

with Image.open("input.jpg") as image:
    buffer = BytesIO()
    image.save(buffer, format="PNG")
    png_bytes = buffer.getvalue()

# png_bytes is ready for an HTTP response, database field, or other binary API
print(len(png_bytes))

When saving to a stream, specify format explicitly because there is no filename extension for Pillow to inspect. If you need to read the stream again, call buffer.seek(0) before passing it to another consumer.

Return a stream for an upload

from io import BytesIO
from PIL import Image

def png_stream(path: str) -> BytesIO:
    stream = BytesIO()
    with Image.open(path) as image:
        image.save(stream, format="PNG")
    stream.seek(0)
    return stream

stream = png_stream("input.jpg")
# Example: pass stream to an upload client that accepts a binary file object
payload = stream.read()

Save a Matplotlib chart or figure

For a chart, save the Figure rather than taking a screenshot of the display. Matplotlib accepts a path or a file-like object and provides save options for resolution, layout, transparency, and format.

import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot([1, 2, 3], [1, 4, 9])
fig.savefig("plot.png", dpi=300, bbox_inches="tight")
plt.close(fig)

dpi=300 requests a higher raster resolution, while bbox_inches="tight" trims extra margins around the figure. Closing the figure is useful in scripts that create many plots because it releases Matplotlib’s figure resources.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose a format explicitly

import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.bar(["A", "B"], [3, 5])
fig.savefig("chart.webp", format="webp", dpi=150)
plt.close(fig)

The format must be supported by the Matplotlib installation and its backend. If a format is not available, save to a commonly supported format such as PNG or PDF instead.

Write a figure to memory

from io import BytesIO
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot([0, 1, 2], [0, 1, 4])
buffer = BytesIO()
fig.savefig(buffer, format="PNG", dpi=200, bbox_inches="tight")
plt.close(fig)
buffer.seek(0)
image_bytes = buffer.getvalue()

As with Pillow, a file-like destination requires an explicit format. The resulting bytes can be returned by a web endpoint or written to another binary destination.

Save an OpenCV image array

OpenCV’s cv2.imwrite(filename, image, params=...) writes an image array to a filename. The extension determines the output format, and the optional params sequence carries encoder-specific settings.

import cv2

image = cv2.imread("input.jpg")
if image is None:
    raise FileNotFoundError("OpenCV could not read input.jpg")

ok = cv2.imwrite("output.png", image)
if not ok:
    raise OSError("Image could not be written")

Do not ignore the return value: OpenCV reports a failed write with False. Also remember that OpenCV commonly represents color arrays in BGR order, while Pillow and many other tools use RGB. Convert when handing an array between those libraries.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import cv2
from PIL import Image

bgr = cv2.imread("input.jpg")
if bgr is None:
    raise FileNotFoundError("input.jpg was not readable")
rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)
Image.fromarray(rgb).save("converted.png")

Pick PNG, JPEG, WebP, or another format

  • PNG: lossless and suitable for screenshots, text, diagrams, and transparency. Files can be larger than lossy formats.
  • JPEG: lossy and generally suited to photographs. It does not preserve transparency, so convert an alpha image to RGB first.
  • WebP: can provide lossy or lossless output when the installed writer supports it; verify that the receiving system accepts it.
  • PDF or vector output: Matplotlib can save figures in formats supported by its backend; use a vector format when preserving scalable chart geometry matters.

Match the extension to the intended format. For Pillow and Matplotlib streams, pass format= explicitly. For OpenCV, use an extension that selects the encoder you intend.

Save reliably in real programs

Create the destination directory

from pathlib import Path
from PIL import Image

output = Path("exports") / "image.png"
output.parent.mkdir(parents=True, exist_ok=True)
with Image.open("input.jpg") as image:
    image.save(output)

Creating the parent directory avoids a failure when the output folder does not yet exist. Use absolute paths when a service’s working directory is not predictable.

Preserve metadata only when appropriate

Converting between formats can change metadata and color information. If metadata matters, check the target format’s capabilities and Pillow or Matplotlib options for that writer. Do not assume that a successful save means every camera profile, EXIF field, or alpha channel survived.

Validate the result

from pathlib import Path
from PIL import Image

path = Path("output.png")
if not path.is_file() or path.stat().st_size == 0:
    raise OSError("No usable output file was created")

with Image.open(path) as saved:
    saved.verify()

verify() checks file integrity without decoding every pixel for later processing. Reopen the image normally if you need dimensions or pixel data after verification.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Common errors and fixes

  • “unknown file extension” (Pillow): the destination suffix is not recognized. Add format="PNG" or another explicit format.
  • “cannot write mode RGBA as JPEG”: convert with image.convert("RGB"), or choose PNG/WebP when transparency is required.
  • OpenCV returns False: check that the parent directory exists, the path is writable, the extension is supported, and the array is valid.
  • Output is blank or unexpectedly cropped: for Matplotlib, save the figure object after plotting and review bbox_inches, layout, and transparent-background settings.
  • Colors look wrong after moving between OpenCV and Pillow: convert BGR to RGB (or RGB to BGR) explicitly.
  • Memory use grows while saving many plots: close each Matplotlib figure with plt.close(fig) after savefig().
  • Permission denied: choose a writable directory or correct the process account’s permissions; do not run with elevated privileges as a routine fix.

Or skip the browser setup

If the image you need is a webpage screenshot rather than a local image object, ScreenshotNeo returns PNG, JPEG, WebP, or PDF from one GET request. It accepts cookie and consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; those steps can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and each response identifies the page verdict and billing status in X-Page-Verdict and X-Billed headers.

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

See the ScreenshotNeo API documentation for the complete option set, including full-page lazy-image loading, CSS-selector element capture, dark mode, device presets and custom viewports, retina scale, PDF paper and page controls, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage reporting, and the OpenAPI specification. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

The Free plan includes 1,000 shots each month with no card. Paid plans start at $5 for 3,000 shots; every feature is on every plan, and yearly billing gives two months free. Create a free ScreenshotNeo account to try it.

Practical decision checklist

  1. Identify whether the object is a Pillow image, Matplotlib figure, OpenCV array, or raw bytes.
  2. Choose the output format based on transparency, photographic quality, scalability, and the receiving system.
  3. Save to a path for a durable file, or to a binary stream for an upload or HTTP response.
  4. Make parent directories, explicit formats, and color conversions deliberate rather than implicit.
  5. Check the writer’s return value or reopen the result before treating the save as successful.

Frequently Asked Questions

Can Python save an image directly from memory?

Yes. Use a binary io.BytesIO object, call Pillow’s or Matplotlib’s save method with an explicit format, then read the buffer’s bytes.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why does Pillow need a format when saving to BytesIO?

A memory buffer has no filename extension, so Pillow cannot infer the encoder. Pass format="PNG", "JPEG", or another supported format.

Which method should I use for a Matplotlib plot?

Use fig.savefig() (or plt.savefig()) because it exposes figure-specific options such as DPI, bounding boxes, and transparency.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.