ScreenshotNeo

BlogScreenshots on your device

How to Take Screenshots with Pillow ImageGrab in Python

Capture your full desktop, a region, or a window with Pillow ImageGrab, with platform options, Retina handling, troubleshooting, and a hosted API alternative.

By the ScreenshotNeo team29 September 20269 min read

How to Take Screenshots with Pillow ImageGrab in Python

Direct answer: install Pillow, import ImageGrab, call ImageGrab.grab(), and save the returned image. The call captures the whole screen by default. Pass bbox=(left, top, right, bottom) to capture a rectangle.

from PIL import ImageGrab

screenshot = ImageGrab.grab()
screenshot.save("screenshot.png")

For a selected region:

from PIL import ImageGrab

screenshot = ImageGrab.grab(bbox=(100, 100, 800, 600))
screenshot.save("region.png")

ImageGrab.grab() returns a Pillow image object, so you can save it, inspect it, convert its mode, resize it, or pass it to another image-processing step. The exact result depends on your operating system, display server, monitor layout, scaling settings, and installed Pillow version. The official ImageGrab reference documents the full signature and platform behavior.

Install Pillow and verify the capture path

Use the Python environment that will run your script:

python -m pip install --upgrade Pillow

Then create capture.py:

from pathlib import Path
from PIL import ImageGrab

output = Path("screenshot.png")
image = ImageGrab.grab()
print(f"mode={image.mode} size={image.size}")
image.save(output)
print(f"saved {output.resolve()}")

Run it from a graphical session:

python capture.py

The printed mode and dimensions are useful diagnostics. According to Pillow’s API documentation, captures are RGBA on macOS and RGB on other platforms in the normal case. Do not assume a mode before inspecting it.

Capture a region with bbox

bbox is a four-item tuple in screen coordinates: (left, top, right, bottom). The left and top edges are inclusive; the right and bottom edges define the far edge of the rectangle.

ImageGrab can capture the full desktop or a coordinate-defined region and return it as a Pillow image.
ImageGrab can capture the full desktop or a coordinate-defined region and return it as a Pillow image.
from PIL import ImageGrab

left, top, right, bottom = 100, 100, 800, 600
if right <= left or bottom <= top:
    raise ValueError("right must exceed left and bottom must exceed top")

image = ImageGrab.grab(bbox=(left, top, right, bottom))
image.save("editor-region.png")

Use the same coordinate system as the desktop, not coordinates relative to a particular application window. A practical way to discover coordinates is to capture the full display, print its size, and adjust the rectangle while checking the saved output.

Capture multiple monitors and individual windows

All Windows monitors

On Windows, all_screens=True requests a capture spanning all monitors:

from PIL import ImageGrab

image = ImageGrab.grab(all_screens=True)
image.save("all-monitors.png")

With multiple monitors, the virtual desktop’s top-left point can be negative when a monitor sits to the left or above the primary display. Consequently, a bounding box can contain negative coordinates:

from PIL import ImageGrab

# Example only: confirm your monitor arrangement first.
image = ImageGrab.grab(bbox=(-1920, 0, 0, 1080), all_screens=True)
image.save("left-monitor.png")

include_layered_windows is a Windows-only option for including layered windows. Use it only when your capture needs those windows and verify the result on the Windows version you deploy.

One window

The window argument captures one window by native identifier: an HWND on Windows or a CGWindowID on macOS. Support is version-specific: Pillow documented Windows support in 11.2.1 and macOS support in 12.1.0. Older installations may reject the argument.

from PIL import ImageGrab

# Replace WINDOW_ID with a valid native window identifier.
image = ImageGrab.grab(window=WINDOW_ID)
image.save("window.png")

Obtaining a native identifier is operating-system work, so keep this path separate from portable code. If you need a cross-platform application screenshot, use a library designed for that application’s windowing toolkit or capture a known screen rectangle.

macOS Retina scaling and image modes

Retina displays can produce images at twice the logical width and height. Pillow 12.3.0 added the keyword-only scale_down=True option to request 1× sizing for Retina captures:

from PIL import ImageGrab

image = ImageGrab.grab(scale_down=True)
print(image.mode, image.size)
image.save("retina-1x.png")

Use this only when the installed Pillow version supports it. For code that must run with older versions, inspect the version and either omit the option or upgrade Pillow. If a downstream API expects RGB, convert explicitly:

from PIL import ImageGrab

image = ImageGrab.grab()
if image.mode != "RGB":
    image = image.convert("RGB")
image.save("rgb-screenshot.jpg", quality=92)

Converting RGBA to RGB removes the alpha channel. Keep PNG when transparency or lossless pixels matter; use JPEG for smaller photographic screenshots when an opaque background is acceptable.

Linux display requirements and fallbacks

On Linux, ImageGrab.grab() uses an X11 display path when xdisplay=None. If the default X11 capture does not return a snapshot, Pillow may use gnome-screenshot, grim, or spectacle when installed. Pass xdisplay="" to disable that fallback behavior:

from PIL import ImageGrab

# Force Pillow's direct path instead of external fallback utilities.
image = ImageGrab.grab(xdisplay="")
image.save("x11-capture.png")

Check whether Pillow has XCB support:

from PIL import features
print(features.check_feature(feature="xcb"))

A headless SSH session, a container without a display, Wayland policy, or missing helper utility can all prevent a capture. Confirm that the process has a usable graphical session before changing application code.

Build a reusable command-line capture script

This example supports a full screen or a validated rectangle, reports image metadata, and creates the parent directory:

#!/usr/bin/env python3
import argparse
from pathlib import Path
from PIL import ImageGrab

parser = argparse.ArgumentParser()
parser.add_argument("--output", default="screenshot.png")
parser.add_argument("--bbox", nargs=4, type=int, metavar=("LEFT", "TOP", "RIGHT", "BOTTOM"))
parser.add_argument("--all-screens", action="store_true")
parser.add_argument("--scale-down", action="store_true")
args = parser.parse_args()

bbox = tuple(args.bbox) if args.bbox else None
if bbox and (bbox[2] <= bbox[0] or bbox[3] <= bbox[1]):
    parser.error("RIGHT must exceed LEFT and BOTTOM must exceed TOP")

kwargs = {"bbox": bbox, "all_screens": args.all_screens}
if args.scale_down:
    kwargs["scale_down"] = True

image = ImageGrab.grab(**kwargs)
output = Path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)
image.save(output)
print(f"saved={output.resolve()} mode={image.mode} size={image.size}")

Examples:

python capture.py --output out/full.png
python capture.py --bbox 100 100 800 600 --output out/panel.png
python capture.py --all-screens --output out/desktops.png
python capture.py --scale-down --output out/retina-1x.png

Options at a glance

Option Use Availability or caveat
bbox Capture a rectangle Uses desktop coordinates
all_screens Capture every monitor Windows; virtual coordinates may be negative
include_layered_windows Include layered windows Windows-only
window Capture one native window HWND on Windows; CGWindowID on macOS; version-specific
xdisplay Select or disable X11/fallback display path Linux behavior
scale_down Request 1× Retina output Added in Pillow 12.3.0

Reliability, performance, and file size

  • Capture cost: memory use grows with pixel count. A 4K all-monitor capture can be substantially larger than a small bbox.
  • Reduce work: capture only the region you need, use scale_down=True on supported Pillow versions, and resize after capture when a fixed output size is required.
  • Save deliberately: PNG preserves sharp text and UI edges. JPEG is smaller but introduces artifacts. WebP can provide a size compromise when your consumers support it.
  • Validate output: check image.size, image.mode, and the file’s existence before uploading or processing it.
  • Repeatability: desktop content changes while a capture runs. For automated visual checks, hide notifications, stabilize the target window, and capture a known region.
  • Permissions: operating-system privacy controls can block screen recording or window capture. The research documentation describes API behavior, not a universal permission prompt or policy for every desktop release.

Troubleshooting common errors

“OSError: screen grab failed” or an empty result

Cause: no usable graphical session, unavailable display server, denied permission, or a Linux capture helper that is not installed. Fix: run inside the logged-in desktop session, verify the display environment, check XCB support, and install or configure the documented Linux capture path. Try xdisplay="" only when you want to disable fallback utilities.

The image is the wrong size on macOS

Cause: Retina scaling can produce 2× pixel dimensions. Fix: upgrade to Pillow 12.3.0 or newer and use scale_down=True, or resize explicitly after inspecting image.size.

The rectangle is shifted or blank

Cause: bbox coordinates were measured relative to an application instead of the desktop, or a monitor has a negative virtual coordinate. Fix: map the rectangle to the desktop coordinate system and confirm the full virtual-screen layout with all_screens=True on Windows.

“unexpected keyword argument ‘window’”

Cause: the installed Pillow version predates support for that operating system. Fix: upgrade Pillow, or capture the window’s screen rectangle instead.

Colors or transparency are wrong

Cause: macOS commonly returns RGBA while other platforms commonly return RGB. Fix: inspect image.mode and convert deliberately with convert("RGB") or convert("RGBA").

Linux works locally but fails in a service

Cause: the service has no interactive display or does not inherit the required display authorization. Fix: run the capture in a desktop-aware worker, configure the display access required by your environment, or use a browser/API capture service for remote web pages.

Or skip the browser setup

ScreenshotNeo captures web pages through one HTTP request, so it is a better fit when your input is a URL rather than your current desktop. It removes cookie and consent banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing result. It also provides an MCP server for Claude, Cursor, and other MCP clients.

A hosted browser capture can remove common consent and overlay elements before saving the page.
A hosted browser capture can remove common consent and overlay elements before saving the page.

See the ScreenshotNeo API documentation for the complete option list. A basic WebP capture:

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

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)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', data);

ScreenshotNeo supports full-page captures with lazy images loaded, CSS-selector element captures, dark mode, device presets or custom viewports, Retina scale, PDF output, custom CSS and JavaScript, clicks, selector or network-idle waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, asynchronous jobs, webhooks, bulk capture, usage reporting, and an OpenAPI specification. Parameter names used by other screenshot APIs also work, which can simplify migration.

Free accounts include 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.

Choosing desktop capture versus URL capture

Need Use
Current monitor, application, or local desktop Pillow ImageGrab
One rectangle or all Windows monitors Pillow with bbox or all_screens=True
A public or authenticated web URL ScreenshotNeo or another browser-based capture workflow
Repeatable remote captures without desktop setup ScreenshotNeo with waits, headers, cookies, blocking, caching, and async jobs

Pillow captures pixels visible to a desktop session. It does not render a URL independently, remove web consent UI, or provide browser navigation controls. A hosted browser API handles those web-page concerns while ImageGrab remains appropriate for local screen pixels.

FAQ

Does ImageGrab capture the whole desktop by default?

Yes. Omit bbox for the full available screen, or provide a four-coordinate box for a region.

Can I capture a browser page with Pillow?

Only what is visible in the browser window or selected screen rectangle. For a URL-level capture with page waits and web cleanup, use a browser-based service such as ScreenshotNeo.

Why is my screenshot twice as large on a Mac?

Retina captures can use 2× pixel dimensions. Pillow 12.3.0 and newer support scale_down=True for 1× output.

Can ImageGrab run on a headless server?

It requires an accessible graphical capture path. A headless worker without a display generally needs a virtual display or a different capture approach.

Which format should I save?

Use PNG for crisp interface text and lossless pixels; JPEG for smaller opaque photographic images; choose another format only when your downstream consumer supports it.

Primary references