How to Use Selenium Screenshots for Visual Regression Testing in Python
Capture pages with Selenium, compare them with reviewed PNG baselines, and make visual tests more repeatable in Python.
Selenium can capture the current browser window as a PNG with driver.save_screenshot(path). For visual regression testing, capture the same page state on each run, compare the new image with a reviewed baseline, and inspect differences before accepting a baseline update. Selenium captures screenshots; it does not provide the image comparison or baseline review workflow, so this guide supplies a small local Python comparator.
The example below uses Selenium to open a page, wait for a meaningful element, set a consistent viewport, and save a screenshot. Pillow compares that image with a baseline and writes a difference image when pixels exceed a chosen tolerance.
1. Install the dependencies
Use the same Python environment in local development and CI. Selenium’s Python API documentation shows the WebDriver screenshot methods used below. Selenium screenshot examples · Python WebDriver API.
python -m venv .venv
. .venv/bin/activate
python -m pip install selenium Pillow
On Windows PowerShell, activate with .venv\Scripts\Activate.ps1. Selenium needs a compatible browser and driver. Keep the browser version and operating environment consistent between baseline creation and later runs; the particular driver installation method depends on your environment.
2. Capture and compare a page
Save this as visual_test.py. Replace the example URL and selector with a page and stable element in your application. Run it with --update to create or intentionally replace the baseline. Normal runs compare against that baseline and return a nonzero exit status when the image differs beyond the configured limit.
import argparse
import sys
from pathlib import Path
from PIL import Image, ImageChops
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
URL = "https://example.com/"
READY_SELECTOR = "main"
BASELINE = Path("visual-baselines/home.png")
ACTUAL = Path("artifacts/home-actual.png")
DIFF = Path("artifacts/home-diff.png")
VIEWPORT = (1365, 900)
def capture() -> None:
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument(f"--window-size={VIEWPORT[0]},{VIEWPORT[1]}")
driver = webdriver.Chrome(options=options)
try:
driver.set_window_size(*VIEWPORT)
driver.get(URL)
WebDriverWait(driver, 20).until(
EC.visibility_of_element_located((By.CSS_SELECTOR, READY_SELECTOR))
)
# Reduce animation differences during the capture. This does not freeze
# every possible source of dynamic content on the page.
driver.execute_script("""
const style = document.createElement('style');
style.textContent = `*, *::before, *::after {
animation: none !important;
transition: none !important;
caret-color: transparent !important;
}`;
document.head.appendChild(style);
""")
ACTUAL.parent.mkdir(parents=True, exist_ok=True)
if not driver.save_screenshot(str(ACTUAL)):
raise OSError(f"Selenium could not save screenshot to {ACTUAL}")
finally:
driver.quit()
def compare(max_changed_fraction: float, channel_tolerance: int) -> bool:
if not BASELINE.is_file():
raise FileNotFoundError(
f"Missing baseline {BASELINE}. Review a capture, then run with --update."
)
if not ACTUAL.is_file():
raise FileNotFoundError(f"Missing actual screenshot {ACTUAL}")
baseline = Image.open(BASELINE).convert("RGB")
actual = Image.open(ACTUAL).convert("RGB")
if baseline.size != actual.size:
print(f"Size mismatch: baseline={baseline.size}, actual={actual.size}")
return False
# Mark a pixel changed if any RGB channel differs by more than tolerance.
delta = ImageChops.difference(baseline, actual)
changed = delta.point(lambda value: 255 if value > channel_tolerance else 0)
changed_pixels = sum(1 for pixel in changed.getdata() if pixel)
total_pixels = baseline.width * baseline.height
fraction = changed_pixels / total_pixels
print(f"Changed pixels: {changed_pixels}/{total_pixels} ({fraction:.3%})")
if fraction > max_changed_fraction:
DIFF.parent.mkdir(parents=True, exist_ok=True)
# A visual diff helps locate changed areas; retain actual and diff as CI artifacts.
ImageChops.difference(baseline, actual).save(DIFF)
print(f"Visual difference exceeds limit. Inspect {ACTUAL} and {DIFF}.")
return False
return True
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--update", action="store_true", help="replace the reviewed baseline")
parser.add_argument("--max-changed-fraction", type=float, default=0.001,
help="maximum changed-pixel fraction, from 0 through 1")
parser.add_argument("--channel-tolerance", type=int, default=8,
help="per-channel RGB difference treated as insignificant, 0 through 255")
args = parser.parse_args()
if not 0 <= args.max_changed_fraction <= 1:
parser.error("--max-changed-fraction must be between 0 and 1")
if not 0 <= args.channel_tolerance <= 255:
parser.error("--channel-tolerance must be between 0 and 255")
capture()
if args.update:
BASELINE.parent.mkdir(parents=True, exist_ok=True)
BASELINE.write_bytes(ACTUAL.read_bytes())
print(f"Updated baseline: {BASELINE}. Review this change before committing it.")
return 0
try:
return 0 if compare(args.max_changed_fraction, args.channel_tolerance) else 1
except (OSError, ValueError) as error:
print(f"Visual test could not complete: {error}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())
# Create an initial baseline after reviewing the screenshot
python visual_test.py --update
# Compare a new capture with the existing baseline
python visual_test.py
The default changed-pixel fraction and channel tolerance are example policy choices, not universal thresholds. A small tolerance can ignore minor color variation, while a larger fraction permits more changed area. Tune them for your page and rendering environment, then review representative diff artifacts to ensure the test still catches changes that matter.
3. Make captures repeatable
A pixel comparison only says that rendered images differ. It cannot decide whether a difference is a bug. Reproducibility and review are therefore part of the test design.
- Pin the environment. Keep browser, driver, operating system, fonts, and viewport dimensions consistent between baseline creation and test runs.
- Control the page state. Use stable test data, deterministic routes, and a wait for the actual content your test needs. A fixed sleep may be too short on a slow run and unnecessarily long on a fast one.
- Handle dynamic regions narrowly. Freeze animations where practical. Mask or ignore only known volatile regions, such as a timestamp or rotating promotion, and preserve the rest of the image so meaningful changes remain detectable.
- Keep baselines reviewable. Store them in version control or another reviewable location. Treat each baseline change as a change to expected appearance; do not automatically replace a baseline every time comparison fails.
- Retain failure artifacts. Keep the baseline, actual capture, and diff image as CI artifacts so a reviewer can see the failure.
- Choose full-page or focused capture deliberately. A whole-window image covers more layout but can include unrelated volatility. A component screenshot narrows the assertion to the UI under test.
Selenium’s official examples show both a driver screenshot and an element screenshot. The API’s save_screenshot(filename) returns a boolean, so check it or verify the artifact before comparing it. Python WebDriver API.
4. Capture a specific element
When the regression assertion concerns one component, save that element rather than the whole browser window. Replace the capture call after the wait with the following snippet:
element = WebDriverWait(driver, 20).until(
EC.visibility_of_element_located((By.CSS_SELECTOR, "[data-testid='pricing-card']"))
)
if not element.screenshot("artifacts/pricing-card.png"):
raise OSError("Element screenshot could not be saved")
Element capture reduces unrelated page content in the comparison. The component still needs stable content and dimensions. If the target is outside the viewport or changes size as it loads, wait for its final state and check the resulting image dimensions.
5. Integrate the workflow into CI
- Install the pinned Python dependencies and use a consistent browser environment.
- Run the capture and comparison command without
--update. - On failure, retain
artifacts/home-actual.pngandartifacts/home-diff.png, along with the baseline, for review. - When a product change intentionally alters appearance, regenerate the baseline, inspect the image, and commit the reviewed baseline change.
- Keep baseline updates separate enough in code review that reviewers can understand why the expected image changed.
The script uses local disk paths for clarity. In a CI system, configure its artifact collection to preserve the actual and diff images even when the test exits with status 1. Do not let a failed capture silently pass: missing baselines, absent screenshots, and save errors should be treated as test setup failures.
6. Use a managed snapshot workflow when useful
Percy’s Python Selenium integration documents the percy_snapshot(driver, name) call and controls such as full-page capture, animation handling, ignored regions, and element scopes. Snapshot names should be unique. Confirm its current setup and supported options in the repository before adopting it. A managed workflow can suit teams that want review-oriented snapshots; a local pipeline keeps capture, comparison, and baseline storage under project control.
Compare any managed option on Python and Selenium support, baseline storage and approval flow, CI integration, dynamic-region handling, capture scope, diff review, comparison sensitivity, and current cost and terms. Applitools material surfaced for this topic describes a Playwright integration, so it does not establish a drop-in Selenium/Python integration. Applitools Playwright integration.
7. Or skip the browser setup
If your goal is to capture a rendered page rather than run a browser interaction sequence in your test, ScreenshotNeo provides a screenshot API and MCP server for developers. This is a capture option; it does not replace visual comparison, reviewed baselines, or your regression policy. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
- Cookie banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
- Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed. Response headers report the page verdict and billing status.
- An MCP server lets AI agents use screenshot, page-info, and PDF capture tools.
- 1,000 screenshots a month are free with no card. Paid plans start at $5 for 3,000 screenshots; every feature is on every plan.
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
save_screenshot returns false or no file appears |
The output directory is missing, the path is not writable, or the browser could not write the artifact. | Create the parent directory, use a writable full path, check the boolean return, and verify the file exists before comparison. |
| WebDriver cannot start | Browser or driver is missing, incompatible, or unavailable in the execution environment. | Install a compatible browser and driver using the method supported by your environment; keep versions consistent across baseline and CI runs. |
| Wait times out | The selector is wrong, the page failed to load, or the expected element never became visible. | Confirm the URL and selector, inspect the page state, and wait for an element that represents readiness for this test. |
| Many failures show only small differences | Fonts, browser version, viewport, animation, dynamic data, or rendering environment vary. | Stabilize the environment and application state. Freeze animation where practical and narrowly mask known changing regions. |
| Image sizes differ | The page or browser window produced a different capture size, often due to viewport or layout changes. | Set a fixed window size, keep the environment stable, and inspect whether the size change is itself a regression. |
| Comparison fails after an intentional redesign | The reviewed baseline still represents the previous expected appearance. | Run with --update only after reviewing the new capture, then review and commit the baseline change. |
| The test passes despite a visible issue | The changed-pixel threshold is too permissive, or a mask/ignored area hides the affected content. | Lower the allowed changed fraction or channel tolerance, tighten masks, and verify the diff against changes the test should detect. |
| Large pages are slow or unstable to capture | The browser must render more content, or the page depends on slow resources and dynamic loading. | Capture only the relevant element when appropriate, wait for the required state, and remove unnecessary test data or resources where possible. |
9. Performance, reliability, and cost
Local Selenium capture consumes time and resources to start a browser and render a page. Whole-window images and repeated browser startup can increase CI time; focused element screenshots can reduce the comparison scope. Reuse of browser sessions may reduce startup work, but each test must restore a known state and always quit the driver, including on failure.
Reliability depends on reproducible inputs more than on the screenshot call itself. Pin the environment, control test data, wait for a meaningful state, retain failure artifacts, and review baseline changes. Pixel comparison is intentionally simple: font rasterization or antialiasing can produce differences even when layout looks alike, while a permissive threshold can miss real changes. Tune tolerance with representative diffs and keep it under review.
The local example uses Selenium and Pillow, so its direct software cost depends on your existing environment and infrastructure; this guide does not assert a particular service price. Managed snapshot services may have changing plans and terms, which should be checked with the provider. ScreenshotNeo’s listed plans are Free for 1,000 shots a month, Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; yearly billing gives two months free. Every feature is on every plan.
10. FAQ
Does Selenium include a visual regression comparator?
The Selenium API cited here documents screenshot capture. The example adds a separate Pillow comparison; another workflow can use a managed snapshot integration.
Should every pixel difference fail the test?
That is a project policy choice. Exact comparison is sensitive to rendering variation, while tolerance can conceal small defects. Choose a threshold by reviewing actual diffs for your application.
Can I use this for PDF output?
This guide compares browser screenshots as PNG images. PDF visual testing needs a separate PDF capture and comparison workflow.
When should I choose an element screenshot?
Use one when the behavior under test belongs to a component and unrelated page content would add noise. Use a page capture when layout across the page is part of the assertion.


