Selenium Python Screenshots: save_screenshot Done Right
Take a screenshot with Selenium in Python: save_screenshot, headless Chrome, exact window size, WebP output and failure screenshots in pytest. Tested code.
To take a screenshot with Selenium in Python, start Chrome, open the page and call save_screenshot:
from selenium import webdriver options = webdriver.ChromeOptions() options.add_argument('--headless=new') driver = webdriver.Chrome(options=options) try: driver.get('https://example.com') driver.save_screenshot('page.png') finally: driver.quit()
That works, but the image it writes is 756×413 pixels, in the dark theme if your laptop uses one. This guide fixes the size, the theme, the format and the error handling, and ends with a pytest hook that saves a screenshot when a test fails. It is the Python part of our Selenium screenshots guide for Python, Java and C#; go there for the API map across languages and the PDF command.
Tested on 2026-10-18 with Python 3.15.0,
selenium==4.51.0,pillow==13.0.0andpytest==9.1.1on macOS 27.0.1 (Apple M1 Pro), Google Chrome 155.0.8059.80. Every listing on this page ran againsthttps://example.com.
Install: no driver download
python3 -m venv .venv source .venv/bin/activate pip install selenium==4.51.0
That is all. Selenium Manager, a small binary inside the selenium wheel, runs when you call webdriver.Chrome(). It looks for Chrome, finds a chromedriver that matches its major version, downloads one if needed and caches it in ~/.cache/selenium. Our cache held six driver builds from 153 to 155 after a few months of use. You can delete old ones.
Remove webdriver-manager and any executable_path= arguments from old code. executable_path was removed from the Chrome() constructor in Selenium 4.10. If you need a specific driver binary, pass a service: webdriver.Chrome(service=Service('/usr/local/bin/chromedriver'), options=options).
When the download index cannot be reached, Selenium Manager logs There was an error managing chromedriver (...); using driver found in the cache and continues with the cached driver. On a fresh machine with no cache there is nothing to fall back to, and the driver cannot start. Bake the driver into CI images that run without internet access, and rebuild them when Chrome updates, or you get a "session not created" error.
Headless: the attribute that silently does nothing
Old tutorials set headless mode like this:
options = webdriver.ChromeOptions() options.headless = True # does nothing in Selenium 4.10+
In Selenium 4.51 this line raises no error and changes nothing. We checked options.arguments after it: the list was empty, and Chrome opened a normal window. On a server without a display, the result is a crash at startup, far from the line that caused it. The correct form is an argument:
options.add_argument('--headless=new')
You may also see --headless=old. Chrome removed the old headless mode from the main browser binary, and Chrome 155 accepted the flag but behaved exactly like the new mode in our test: same user agent (HeadlessChrome/155.0.0.0), same window metrics, same image. The old implementation lives on as a separate download, chrome-headless-shell, which Selenium can drive if you set options.binary_location to its path. We tried it with chrome-headless-shell 156 and the cached chromedriver 155, and it worked.
Get the size you asked for
save_screenshot captures the viewport, the part of the page inside the window. In headless Chrome the window and the viewport are not the same size:
| Code | Screenshot |
|---|---|
| No size set | 756×413 |
driver.set_window_size(1280, 800) |
1280×657 |
options.add_argument('--window-size=1280,800') |
1280×657 |
Headless Chrome subtracts 143 px of height for a toolbar it never draws. The cleanest cross-browser fix is to measure that gap once and grow the window by it:
"""Selenium 4 screenshot of a page at an exact 1280x800 viewport. Usage: python screenshot.py [URL] [OUT.png] """ import sys from selenium import webdriver WIDTH, HEIGHT = 1280, 800 def set_viewport(driver, width, height): # The window includes space for browser UI, even in headless mode. # Measure that space and grow the window by it. extra_w, extra_h = driver.execute_script( 'return [outerWidth - innerWidth, outerHeight - innerHeight]' ) driver.set_window_size(width + extra_w, height + extra_h) def main(): url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com' out = sys.argv[2] if len(sys.argv) > 2 else 'screenshot.png' options = webdriver.ChromeOptions() options.add_argument('--headless=new') options.add_argument('--hide-scrollbars') driver = webdriver.Chrome(options=options) # Selenium Manager finds the driver try: set_viewport(driver, WIDTH, HEIGHT) driver.get(url) if not driver.save_screenshot(out): sys.exit(f'could not write {out}') print(out, driver.execute_script('return [innerWidth, innerHeight]')) finally: driver.quit() if __name__ == '__main__': main()
It printed out/s.png [1280, 800] and the PNG was 1280×800. Because the helper only uses execute_script and set_window_size, it also works with Firefox and Edge drivers. In Chrome you can instead call driver.execute_cdp_cmd('Emulation.setDeviceMetricsOverride', {...}), which also sets the device scale factor; the Selenium hub guide uses that form.
--hide-scrollbars matters on pages taller than the window. Without it, a scrollbar can take a strip of the right edge on platforms that draw classic scrollbars.
For a 2× retina image, add options.add_argument('--force-device-scale-factor=2'). The 1280×800 viewport then gives a 2560×1600 PNG, and window.devicePixelRatio reports 2.
Bytes, base64 and other formats
Three methods return the same PNG in different shapes:
| Method | Returns | Use it for |
|---|---|---|
save_screenshot(path) |
True or False |
Writing a file |
get_screenshot_as_png() |
bytes |
Pillow, uploads, hashing |
get_screenshot_as_base64() |
str |
HTML reports, JSON payloads |
save_screenshot returns False when it cannot write the file, for example when the folder does not exist, and raises no exception. Check the return value, as the listing above does.
The format is always PNG. We saved to wrong.jpg and got True, a warning (name used for saved screenshot does not match file type. It should end with a .png extension) and a file whose first bytes were the PNG signature. Image viewers cope, but a CDN or an upload validator that checks the extension against the content will not. Convert with Pillow instead:
import io from PIL import Image from selenium import webdriver options = webdriver.ChromeOptions() options.add_argument('--headless=new') driver = webdriver.Chrome(options=options) try: driver.execute_cdp_cmd('Emulation.setDeviceMetricsOverride', { 'width': 1280, 'height': 800, 'deviceScaleFactor': 1, 'mobile': False, }) driver.get('https://example.com') png = driver.get_screenshot_as_png() finally: driver.quit() image = Image.open(io.BytesIO(png)) image.save('page.webp', quality=80) image.save('page.jpg', quality=80) print(image.size, len(png))
The PNG was 81,121 bytes, the WebP 35,540 and the JPEG 64,665. Chrome's screenshot opened in Pillow as mode RGB, so it saves to JPEG directly. If you composite it onto a transparent canvas first, call convert('RGB') before saving a JPEG, because JPEG has no alpha channel.
Element screenshots
Every WebElement has its own screenshot methods: element.screenshot(path), element.screenshot_as_png and element.screenshot_as_base64. The last two are properties, not methods, which trips people up after using the driver's get_screenshot_as_png():
from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait paragraph = WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.CSS_SELECTOR, 'p')) ) paragraph.screenshot('paragraph.png') # method png = paragraph.screenshot_as_png # property, no parentheses
Wait for visibility, not presence. A zero-size element raised WebDriverException: ... Cannot take screenshot with 0 width. in our test. A tall element is captured in full: a 2,000 px element in an 800 px viewport came back 300×2000.
Errors that should stop your script
Selenium raises where the Chrome command line stays quiet. For a host that does not resolve, driver.get() raised WebDriverException: Message: unknown error: net::ERR_NAME_NOT_RESOLVED and our script exited with status 1. Let that exception reach the top of your script, or catch it and exit non-zero.
A slow page is the other case. The default page-load timeout is 300 seconds; we read it from driver.timeouts.page_load. Against a test page with one image that never finished loading, driver.set_page_load_timeout(10) raised TimeoutException after 10.0 s. The DOM was already there, so save_screenshot after the exception still worked. With options.page_load_strategy = 'eager', driver.get() returned in 0.06 s with document.readyState at interactive, and you wait for your own element from there. More failure modes, reproduced one by one, are in headless Chrome production failures.
A 404 or 500 page is not an error to Selenium. It captures the error page like any other. WebDriver does not expose the status code, so if it matters, check it with requests or urllib before you call the browser.
Screenshots on test failure with pytest
The most common reason to take a Selenium screenshot in Python is a failing test. This conftest.py saves one for every failed test that uses the driver fixture:
import pathlib import pytest from selenium import webdriver ARTIFACTS = pathlib.Path('artifacts') @pytest.fixture def driver(): options = webdriver.ChromeOptions() options.add_argument('--headless=new') drv = webdriver.Chrome(options=options) drv.execute_cdp_cmd('Emulation.setDeviceMetricsOverride', { 'width': 1280, 'height': 800, 'deviceScaleFactor': 1, 'mobile': False, }) yield drv drv.quit() @pytest.hookimpl(wrapper=True) def pytest_runtest_makereport(item, call): report = yield if report.when == 'call' and report.failed: drv = item.funcargs.get('driver') if drv is not None: ARTIFACTS.mkdir(exist_ok=True) path = ARTIFACTS / f'{item.name}.png' drv.save_screenshot(str(path)) report.sections.append(('screenshot', str(path))) return report
With one passing and one failing test, pytest printed a screenshot section under the failure with artifacts/test_wrong_title.png, and only that file was written. The hook runs before fixture teardown, so the browser is still open when it captures. wrapper=True is the current hook-wrapper style; the older hookwrapper=True with outcome.get_result() still works but is the legacy form. Upload the artifacts folder from your CI job to see the page as it was when the assertion failed.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| A window opens in "headless" mode | options.headless = True is ignored |
options.add_argument('--headless=new') |
TypeError: ... unexpected keyword argument 'executable_path' |
Removed in Selenium 4.10 | Delete it, or pass service=Service(path) |
| Screenshot is 756×413 | No size set | set_viewport() helper above |
| Screenshot is 1280×657 | Window size includes hidden UI | Add outerHeight - innerHeight |
.jpg file contains PNG |
Selenium writes PNG only | Convert with Pillow |
TypeError: 'bytes' object is not callable |
element.screenshot_as_png() with parentheses |
It is a property |
save_screenshot returns False |
The target folder does not exist | Create it first |
| Script hangs for 5 minutes | Default page-load timeout is 300 s | driver.set_page_load_timeout(30) |
| Screenshot is dark on your Mac | Headless Chrome follows the OS theme | --blink-settings=preferredColorScheme=1 |
The same screenshot with the Python SDK
If you only need the image and not a browser test, the RenderScreenshot Python SDK replaces the driver, the viewport helper and the Pillow step. The viewport parameters are the real viewport, and WebP is an output option:
import os from renderscreenshot import Client, RenderScreenshotError, TakeOptions client = Client(os.environ['RS_API_KEY']) try: image = client.take( TakeOptions.url('https://example.com') .width(1280) .height(800) .format('webp') .quality(80) .wait_for_selector('p') ) except RenderScreenshotError as e: raise SystemExit(f'{e.code}: {e.message}') with open('page.webp', 'wb') as f: f.write(image)
Keep Selenium for screenshots that belong to a test. For screenshots that are the output, such as previews, archives or reports, sign up and try the call with 150 free credits each month. If what you actually need is a capture of your desktop, not a web page, see Python desktop vs web page screenshots.
Related in this topic
- Selenium Screenshots in Python, Java and C# (Selenium 4) Take a screenshot with Selenium 4 in Python, Java and C#: page, element, exact viewport and PDF. Same test in three languages, timed, with the fixes.
- Selenium Full Page Screenshot: Firefox, Chrome, Stitching Take a full page screenshot in Selenium: Firefox's native call, Chrome via CDP Page.captureScreenshot, or scroll and stitch. Measured limits, times and code.
- ChromeDriver: Session Not Created, Version Mismatch Fix Fix 'session not created: this version of ChromeDriver only supports Chrome version N' in Selenium 4, reproduced in Docker with a tested Python fix.
- DevToolsActivePort File Doesn't Exist: Causes and Fix DevToolsActivePort file doesn't exist in Selenium? New ChromeDriver hides it behind other messages. Reproduced on Chrome 128 to 155 in Docker, with fixes.
- How to Take Website Screenshots with Code: 6 Ways Take a screenshot of a website programmatically: DevTools, the Chrome CLI, Puppeteer, Playwright, Selenium or an API. Timed runs and code for each.
- Chrome Headless Screenshot: Every Flag, Tested chrome --headless --screenshot and --print-to-pdf flag by flag: window size, scale, WebP, PDF paper, old vs new headless, and the Linux sandbox error.