DEV Community

Dennis
Dennis

Posted on

Python Website Screenshots: Selenium, Pyppeteer, and API Approaches

I tried to be short, but thoughts expanded a bit, sorry about that.

You can take a screenshot of any URL in Python using Selenium, Pyppeteer, Playwright, or a screenshot API. Selenium uses the WebDriver protocol and supports Chrome, Firefox, and Safari. Pyppeteer is a Python port of Pyppeteer using Chrome's DevTools Protocol. Screenshot APIs handle everything server-side, so you get an image back from a single HTTP request with no browser to manage.

Here's each method with full working code, followed by a comparison to help you pick the right one.

Method 1: Selenium

Selenium is the oldest and most established browser automation tool for Python. If you've done any web testing, you've probably used it already.

Install

pip install selenium
Enter fullscreen mode Exit fullscreen mode

You also need a browser driver. For Chrome, the easiest path is webdriver-manager:

pip install webdriver-manager
Enter fullscreen mode Exit fullscreen mode

Basic Screenshot

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager

options = Options()
options.add_argument('--headless=new')
options.add_argument('--window-size=1280,720')

service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service, options=options)

driver.get('https://example.com')
driver.save_screenshot('screenshot.png')
driver.quit()
Enter fullscreen mode Exit fullscreen mode

webdriver-manager automatically downloads the right ChromeDriver version for your installed Chrome. Without it, you have to manually match ChromeDriver to your Chrome version, which breaks every time Chrome updates.

Headless Mode

The --headless=new flag uses Chrome's new headless mode (available since Chrome 112). The old --headless flag uses a separate headless implementation that renders some pages differently. Always use --headless=new for accurate screenshots.

options = Options()
options.add_argument('--headless=new')
options.add_argument('--disable-gpu')
options.add_argument('--no-sandbox')
Enter fullscreen mode Exit fullscreen mode

The --no-sandbox flag is needed in Docker and some CI environments. On your local machine, you can skip it.

Full-Page Screenshot Workaround

Here's the frustrating part: Selenium's Chrome WebDriver doesn't natively support full-page screenshots. save_screenshot() captures only the viewport. Firefox does support it via save_full_page_screenshot(), but Chrome doesn't.

The workaround is to resize the browser window to match the page height:

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager

def full_page_screenshot(url, output_path):
    options = Options()
    options.add_argument('--headless=new')
    options.add_argument('--window-size=1280,720')

    service = Service(ChromeDriverManager().install())
    driver = webdriver.Chrome(service=service, options=options)
    driver.get(url)

    # Get the full page dimensions
    total_height = driver.execute_script('return document.body.scrollHeight')
    total_width = driver.execute_script('return document.body.scrollWidth')

    # Resize the window to fit the entire page
    driver.set_window_size(total_width, total_height)

    # Give the page a moment to reflow after resize
    import time
    time.sleep(0.5)

    driver.save_screenshot(output_path)
    driver.quit()

full_page_screenshot('https://example.com', 'full-page.png')
Enter fullscreen mode Exit fullscreen mode

This works, but it's a hack. The page reflows at the new dimensions, so what you capture might look different from what a user sees at normal viewport size. Fixed headers stack at the top, sticky elements pile up, and responsive breakpoints can shift.

Alternatively, you can use the Chrome DevTools Protocol directly through Selenium:

def full_page_screenshot_cdp(url, output_path):
    options = Options()
    options.add_argument('--headless=new')
    options.add_argument('--window-size=1280,720')

    service = Service(ChromeDriverManager().install())
    driver = webdriver.Chrome(service=service, options=options)
    driver.get(url)

    # Use CDP command for full-page capture
    metrics = driver.execute_cdp_cmd('Page.getLayoutMetrics', {})
    width = metrics['contentSize']['width']
    height = metrics['contentSize']['height']

    driver.execute_cdp_cmd('Emulation.setDeviceMetricsOverride', {
        'width': int(width),
        'height': int(height),
        'deviceScaleFactor': 1,
        'mobile': False
    })

    result = driver.execute_cdp_cmd('Page.captureScreenshot', {
        'format': 'png',
        'captureBeyondViewport': True
    })

    import base64
    with open(output_path, 'wb') as f:
        f.write(base64.b64decode(result['data']))

    driver.quit()
Enter fullscreen mode Exit fullscreen mode

More reliable, but now you're mixing WebDriver and CDP, which can get messy.

Waiting for Content

Selenium's default behavior waits for the page load event. For SPAs and dynamic content, you need explicit waits:

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

driver.get('https://spa-app.com')
WebDriverWait(driver, 10).until(
    EC.presence_of_element_located((By.ID, 'main-content'))
)
driver.save_screenshot('spa.png')
Enter fullscreen mode Exit fullscreen mode

Firefox Full-Page (Native Support)

Firefox actually handles full-page screenshots natively:

from selenium import webdriver
from selenium.webdriver.firefox.service import Service
from webdriver_manager.firefox import GeckoDriverManager

options = webdriver.FirefoxOptions()
options.add_argument('--headless')

service = Service(GeckoDriverManager().install())
driver = webdriver.Firefox(service=service, options=options)
driver.get('https://example.com')
driver.save_full_page_screenshot('full-page.png')
driver.quit()
Enter fullscreen mode Exit fullscreen mode

One method call, no hacks. If full-page capture is important and you don't specifically need Chrome, Firefox with Selenium is the simpler path.

Method 2: Pyppeteer

Pyppeteer is a Python port of Node.js Pyppeteer. It uses async/await and talks directly to Chrome via the DevTools Protocol. The API mirrors Pyppeteer closely, so if you've used Pyppeteer in Node.js, Pyppeteer will feel familiar.

Install

pip install pyppeteer
Enter fullscreen mode Exit fullscreen mode

On first run, Pyppeteer downloads Chromium automatically (similar to how Pyppeteer works in Node.js).

Basic Screenshot

import asyncio
from pyppeteer import launch

async def take_screenshot(url, output_path):
    browser = await launch(headless=True)
    page = await browser.newPage()
    await page.setViewport({'width': 1280, 'height': 720})
    await page.goto(url, {'waitUntil': 'networkidle2'})
    await page.screenshot({'path': output_path})
    await browser.close()

asyncio.run(take_screenshot('https://example.com', 'screenshot.png'))
Enter fullscreen mode Exit fullscreen mode

Full-Page Capture

Unlike Selenium, Pyppeteer supports full-page screenshots natively:

await page.screenshot({
    'path': 'full-page.png',
    'fullPage': True
})
Enter fullscreen mode Exit fullscreen mode

Viewport and Mobile Emulation

async def mobile_screenshot(url, output_path):
    browser = await launch(headless=True)
    page = await browser.newPage()
    await page.setViewport({
        'width': 390,
        'height': 844,
        'deviceScaleFactor': 3,
        'isMobile': True,
        'hasTouch': True
    })
    await page.goto(url, {'waitUntil': 'networkidle2'})
    await page.screenshot({'path': output_path})
    await browser.close()
Enter fullscreen mode Exit fullscreen mode

PDF Generation

async def save_as_pdf(url, output_path):
    browser = await launch(headless=True)
    page = await browser.newPage()
    await page.goto(url, {'waitUntil': 'networkidle2'})
    await page.pdf({
        'path': output_path,
        'format': 'A4',
        'printBackground': True,
        'margin': {'top': '20mm', 'bottom': '20mm'}
    })
    await browser.close()
Enter fullscreen mode Exit fullscreen mode

A Note on Pyppeteer's Maintenance Status

Pyppeteer's development has slowed significantly. The last major release was in 2022, and it pins to an older Chromium version. For new projects, Playwright's Python bindings are a better choice (covered below). If you're already using Pyppeteer in production and it works, no reason to migrate. But I wouldn't start a new project with it today.

Playwright for Python

Playwright has first-class Python bindings that deserve mention. It's the most actively maintained option and supports Chromium, Firefox, and WebKit.

Install

pip install playwright
playwright install
Enter fullscreen mode Exit fullscreen mode

Basic Screenshot

from playwright.sync_api import sync_playwright

def take_screenshot(url, output_path):
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page(viewport={'width': 1280, 'height': 720})
        page.goto(url, wait_until='networkidle')
        page.screenshot(path=output_path, full_page=True)
        browser.close()

take_screenshot('https://example.com', 'screenshot.png')
Enter fullscreen mode Exit fullscreen mode

Playwright also has an async API:

from playwright.async_api import async_playwright
import asyncio

async def take_screenshot(url, output_path):
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page(viewport={'width': 1280, 'height': 720})
        await page.goto(url, wait_until='networkidle')
        await page.screenshot(path=output_path, full_page=True)
        await browser.close()

asyncio.run(take_screenshot('https://example.com', 'screenshot.png'))
Enter fullscreen mode Exit fullscreen mode

Playwright's auto-waiting, locator API, and cross-browser support make it the strongest option for new Python projects that need browser automation.

Method 3: Screenshot APIs

All the methods above require running a headless browser. That means managing Chromium binaries, handling process lifecycles, and dealing with memory issues on your server. Screenshot APIs move that complexity off your infrastructure entirely.

Using SnapRender's Python SDK

pip install snaprender
Enter fullscreen mode Exit fullscreen mode
from snaprender import SnapRender

client = SnapRender(api_key='sk_live_...')
image_bytes = client.screenshot(
    url='https://example.com',
    format='png',
    width=1280,
    full_page=True
)

with open('screenshot.png', 'wb') as f:
    f.write(image_bytes)
Enter fullscreen mode Exit fullscreen mode

Ad blocking, cookie banner removal, dark mode, device emulation: all just parameters. No browser to configure, no selectors to maintain.

Using Raw requests

import requests

response = requests.get(
    'https://app.snap-render.com/v1/screenshot',
    headers={'X-API-Key': 'sk_live_...'},
    params={
        'url': 'https://example.com',
        'format': 'png',
        'width': 1280,
        'full_page': True
    }
)

with open('screenshot.png', 'wb') as f:
    f.write(response.content)
Enter fullscreen mode Exit fullscreen mode

No dependencies beyond requests. No Chromium download. The screenshot comes back over HTTP.

Comparison Table

Factor Selenium Pyppeteer Playwright Screenshot API
Install complexity Moderate (driver matching) Low (auto-downloads) Low (auto-downloads) Minimal (pip install)
Full-page capture Workaround needed (Chrome) Native Native Native
Async support No (sync only) Yes (async only) Both sync and async Both (SDK or requests)
Browser support Chrome, Firefox, Safari, Edge Chromium only Chromium, Firefox, WebKit Managed remotely
Community/ecosystem Largest (20+ years) Small, declining Growing fast Varies by provider
Maintenance status Active Stalled Very active Managed service
Memory per instance 150-300 MB 150-300 MB 150-300 MB None (server-side)
Ad/cookie removal Manual scripting Manual scripting Manual scripting Single parameter
Cost Free (compute costs) Free (compute costs) Free (compute costs) Free tier + paid plans

Common Pitfalls

Font rendering differs across environments. A screenshot taken on macOS looks different from one taken on Ubuntu because system fonts differ. If visual consistency matters, you need to install the same fonts on your server, or use a screenshot API that handles font rendering in a consistent environment.

Chrome updates break driver compatibility. Selenium and Pyppeteer are both vulnerable to Chrome auto-updates breaking the driver pairing. webdriver-manager helps for Selenium; Pyppeteer bundles its own Chromium but it's an old version.

Memory usage in production. Each headless browser process uses 150-300 MB. If you're taking screenshots of 10 URLs concurrently with Selenium, that's 1.5-3 GB of RAM just for browsers. In production, you need worker pools, process recycling, and memory monitoring.

Docker environments need extra flags. Running Chrome in Docker requires --no-sandbox, --disable-dev-shm-usage, and sometimes --disable-gpu. The /dev/shm size needs to be increased from the default 64 MB or Chrome crashes silently.

Recommendations

Stick with Selenium if you already have it in your test suite and screenshots are a secondary need. Adding screenshot capture to existing Selenium tests is trivial, and rewriting everything for Playwright isn't worth the migration cost.

Use Playwright for new projects. If I were starting from scratch today, I'd pick Playwright over Pyppeteer or Selenium for any Python project that needs to take a screenshot of a URL. It has the best API design, the most active development, and both sync and async support.

Use a screenshot API for production screenshot features. If screenshots are a product feature (OG images, link previews, visual monitoring, PDF generation for customers), an API is the pragmatic choice. You skip the browser infrastructure entirely, and features like cookie banner removal and ad blocking are already built in.

The right tool depends on what you're building. For testing workflows, a local browser makes sense. For production features where you need to take a screenshot of a URL in Python reliably at scale, the less infrastructure you manage, the better.

Top comments (0)