· Tutorials · By Simon Chiu

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.0 and pytest==9.1.1 on macOS 27.0.1 (Apple M1 Pro), Google Chrome 155.0.8059.80. Every listing on this page ran against https://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