ScreenshotNeo

BlogScreenshots on your device

How to Capture and Save Screenshots From a Python Background Script

Capture and save desktop screenshots from unattended Python scripts with PyAutoGUI, MSS, or Pillow, plus display checks and fixes.

By the ScreenshotNeo team30 September 20266 min read

How to Capture and Save Screenshots From a Python Background Script

Direct answer: If the Python process can access a graphical desktop, save a screenshot with PyAutoGUI:

import pyautogui

image = pyautogui.screenshot('/var/tmp/screenshot.png')
print(image.size)

The filename is written immediately and the call also returns a Pillow image. A background script does not create a display. It can capture only pixels from a monitor, region, or supported window that its account can access. A headless host has no desktop pixels unless you deliberately provide a display session.

Choose the capture target first

Requirement Starting point Verify
One full-screen or rectangular capture PyAutoGUI Pillow and OS prerequisites; coordinates
Repeated captures or explicit monitor selection MSS Display/backend availability and monitor index
Pillow workflow, Windows multi-monitor, or supported window capture ImageGrab Installed Pillow version and OS support

These libraries use different system capture facilities. Compare the target, platform support, dependencies, and processing needs rather than assuming one library is universally faster.

A background process still needs access to a real display before it can save pixels.
A background process still needs access to a real display before it can save pixels.

Install and run PyAutoGUI

python -m pip install pyautogui pillow
python capture_once.py

PyAutoGUI documents scrot as a Linux dependency; macOS uses the system screencapture command. Follow the current instructions for your distribution and installed release in the PyAutoGUI screenshot documentation.

Choose the target—monitor, region, or supported window—before selecting a library.
Choose the target—monitor, region, or supported window—before selecting a library.
from pathlib import Path
import pyautogui

output = Path('/var/lib/my-captures/latest.png')
output.parent.mkdir(parents=True, exist_ok=True)
image = pyautogui.screenshot(str(output))
print(f'wrote {output} ({image.width}x{image.height})')

Use an absolute path for a daemon or scheduled task. Relative paths depend on the process working directory.

Capture a rectangle

import pyautogui

# left, top, width, height
image = pyautogui.screenshot('/var/tmp/dashboard.png', region=(0, 0, 800, 600))

Verify coordinates and bounds on the machine that will run the script.

Use MSS for repeated or monitor-specific captures

MSS exposes monitors and regions and can convert a grabbed frame to a Pillow image. Reuse one MSS instance in a capture loop. See the usage and examples.

from pathlib import Path
from mss import MSS

output_dir = Path('/var/lib/my-captures')
output_dir.mkdir(parents=True, exist_ok=True)

with MSS() as sct:
    monitor = sct.primary_monitor
    image = sct.grab(monitor).to_pil()
    image.save(output_dir / 'monitor.png')
    print(image.size)

For a region, pass a dictionary containing left, top, width, and height to grab(). On Linux, MSS reads DISPLAY by default and accepts an explicit value such as MSS(display=':0.0').

from mss import MSS
from mss.tools import to_png

with MSS() as sct:
    area = {'left': 100, 'top': 100, 'width': 1200, 'height': 800}
    shot = sct.grab(area)
    to_png(shot.rgb, shot.size, output='/var/tmp/region.png')

Capture with Pillow ImageGrab

from PIL import ImageGrab

image = ImageGrab.grab()
image.save('/var/tmp/full.png')

crop = ImageGrab.grab(bbox=(0, 0, 800, 600))
crop.save('/var/tmp/box.png')

Pillow documents all_screens=True on Windows. It also documents a window argument for a single window on Windows (HWND) and macOS (CGWindowID), introduced in Pillow 11.2.1 and 12.1.0 respectively. Confirm your installed version and target OS before relying on those parameters. On Linux, ImageGrab documents fallback to gnome-screenshot, grim, or spectacle when the default X11 display does not return a snapshot. See the ImageGrab reference.

Prepare a background deployment

  1. Test with the same account, environment, and display session as the scheduler or service.
  2. On Linux, inspect DISPLAY and permissions. A headless service should not be assumed to see a desktop.
  3. Use an absolute output path, create its parent directory, and grant the service account write access.
  4. Overwrite a stable filename for the latest image, or add a UTC timestamp or UUID for history.
  5. Restrict output permissions and define a retention policy because screenshots may contain sensitive data.
from datetime import datetime, timezone
from pathlib import Path
import pyautogui

root = Path('/var/lib/my-captures')
root.mkdir(mode=0o700, parents=True, exist_ok=True)
name = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ') + '.png'
path = root / name
pyautogui.screenshot(str(path))
print(path)

Run a scheduled capture loop

import logging
import time
from pathlib import Path
import pyautogui

logging.basicConfig(level=logging.INFO)
out = Path('/var/lib/my-captures/latest.png')
out.parent.mkdir(parents=True, exist_ok=True)

while True:
    try:
        pyautogui.screenshot(str(out))
        logging.info('saved %s', out)
    except Exception:
        logging.exception('capture failed')
    time.sleep(60)

A system scheduler is often easier to supervise than an unbounded loop. Log failures, use a watchdog at the process level, and do not replace a known-good file until a new capture completes.

Common errors and fixes

Symptom Cause Fix
DisplayNotFoundError, black image, or no snapshot No accessible display or wrong DISPLAY Run in the logged-in display session, set the correct display for MSS, and verify permissions.
PyAutoGUI Linux import/runtime error Missing Pillow or OS capture dependency Install Pillow and the dependency required by your distribution.
File not found or permission denied Relative path or unwritable directory Use an absolute path, create the directory, and grant write permission.
Wrong monitor or crop Coordinate origin, scaling, or monitor selection differs Print monitor geometry, test a known rectangle, and select the MSS monitor explicitly.
Window capture unavailable Unsupported OS or Pillow version Check version-specific ImageGrab documentation and fall back to display or region capture.
Old files disappear Stable filename is overwritten Add a timestamp or UUID and implement retention.

Performance, reliability, and cost

  • Performance: PyAutoGUI gives roughly 100 ms as an illustrative 1920×1080 timing. It is not a cross-library benchmark; measure on your hardware.
  • Repeated work: Reuse an MSS context, capture only the needed region, and avoid unnecessary conversions or compression.
  • Reliability: Validate output files, preserve the previous known-good image until replacement completes, and log timestamps and exceptions.
  • Cost: Local libraries have no per-shot API charge, but the machine, display session, storage, and operations are yours to maintain.

Or skip the browser setup

If you need a webpage image rather than pixels from a desktop application, ScreenshotNeo captures a URL with one request. It accepts cookie and consent banners, removes more than 60 known consent platforms plus newsletter popups and chat widgets before capture, and each step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

See the ScreenshotNeo API docs for all options.

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 fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo supports full-page capture with lazy images loaded, CSS-selector element capture, device presets or custom viewports, retina scale, dark mode, custom CSS and JavaScript, waits, request blocking, headers, cookies, user agents, timezone and geolocation, resizing, caching with a chosen TTL, signed links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, usage API, and PDF output. It supports PNG, JPEG, WebP, and PDF. 1,000 shots are free each month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Can a background process capture a hidden application window?

Only where the OS and installed Pillow version support window capture, with the required permissions. Background execution does not make an occluded or nonexistent window visible.

Which library should I start with?

Use PyAutoGUI for a simple full screen or rectangle, MSS for repeated or monitor-specific work, and ImageGrab for a Pillow-centric workflow.

Why does it work manually but fail as a service?

The service likely has a different user, working directory, environment, permissions, or display session. Compare those values and test from the service context.

Can ScreenshotNeo capture my local desktop?

No. ScreenshotNeo captures web URLs. Use PyAutoGUI, MSS, or ImageGrab for local display pixels.