Python Tutorial

How to Screenshot a URL in Python (2026)

April 202612 min readSelenium · Playwright · SnapAPI

Four methods for capturing website screenshots in Python — from Selenium (classic) to Playwright (modern) to a REST API (no browser). Code for every scenario: full-page, mobile, element-level, authenticated pages, and bulk capture.

Method 1: Selenium

Selenium is the oldest and most widely known browser automation tool in Python. It uses the WebDriver protocol and works with Chrome, Firefox, and Edge. For screenshot-only use cases it's heavier than needed, but it's familiar and well-documented.

Install

pip install selenium
# Also need ChromeDriver matching your Chrome version:
pip install webdriver-manager  # Handles driver download automatically

Basic Full-Page Screenshot

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

def screenshot(url: str, output_path: str, width: int = 1280) -> None:
    options = Options()
    options.add_argument('--headless=new')
    options.add_argument('--no-sandbox')
    options.add_argument('--disable-dev-shm-usage')
    options.add_argument(f'--window-size={width},900')

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

    try:
        driver.get(url)
        time.sleep(2)  # Wait for JS to render

        # Get full page height and resize
        total_height = driver.execute_script('return document.body.scrollHeight')
        driver.set_window_size(width, total_height)

        driver.save_screenshot(output_path)
        print(f'Saved: {output_path}')
    finally:
        driver.quit()

screenshot('https://example.com', 'output.png')

Selenium: Element Screenshot

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

def element_screenshot(url: str, selector: str, output_path: str) -> None:
    options = Options()
    options.add_argument('--headless=new')
    options.add_argument('--no-sandbox')
    driver = webdriver.Chrome(options=options)

    try:
        driver.get(url)
        element = WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, selector))
        )
        element.screenshot(output_path)
    finally:
        driver.quit()

element_screenshot('https://example.com', '.hero-section', 'hero.png')
Selenium limitation: Selenium has no native waitUntil: networkidle equivalent. The time.sleep() approach is fragile — too short and JS hasn't rendered, too long and your scraper is slow. Playwright handles this better.

Method 2: Playwright (Recommended)

Playwright for Python is the modern choice. Better async support, native wait_until="networkidle", cross-browser, and no WebDriver compatibility issues.

Install

pip install playwright
python -m playwright install chromium

Basic Screenshot

from playwright.sync_api import sync_playwright

def screenshot(url: str, output_path: str, full_page: bool = True) -> None:
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page(viewport={'width': 1280, 'height': 800})

        page.goto(url, wait_until='networkidle')
        page.screenshot(path=output_path, full_page=full_page)

        browser.close()

screenshot('https://example.com', 'output.png')

Async Playwright (for async apps)

import asyncio
from playwright.async_api import async_playwright

async def screenshot_async(url: str, output_path: str) -> bytes:
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page(viewport={'width': 1280, 'height': 800})

        await page.goto(url, wait_until='networkidle')
        buf = await page.screenshot(full_page=True)

        await browser.close()
        return buf

# Run
buf = asyncio.run(screenshot_async('https://example.com', 'out.png'))
with open('out.png', 'wb') as f:
    f.write(buf)

Mobile Screenshots

from playwright.sync_api import sync_playwright

def mobile_screenshot(url: str, output_path: str) -> None:
    with sync_playwright() as p:
        # Use a built-in device descriptor
        iphone = p.devices['iPhone 14']
        browser = p.chromium.launch(headless=True)
        context = browser.new_context(**iphone)
        page = context.new_page()

        page.goto(url, wait_until='networkidle')
        page.screenshot(path=output_path, full_page=True)

        browser.close()

mobile_screenshot('https://example.com', 'mobile.png')

Authenticated Pages (Session Cookies)

from playwright.sync_api import sync_playwright

def auth_screenshot(url: str, cookies: list, output_path: str) -> None:
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        context = browser.new_context(viewport={'width': 1280, 'height': 800})

        # Inject session cookies before navigating
        context.add_cookies(cookies)

        page = context.new_page()
        page.goto(url, wait_until='networkidle')
        page.screenshot(path=output_path, full_page=True)

        browser.close()

auth_screenshot(
    url='https://app.example.com/dashboard',
    cookies=[{'name': 'session', 'value': 'abc123', 'domain': 'app.example.com', 'path': '/'}],
    output_path='dashboard.png'
)

Bulk Screenshots (Async Parallel)

import asyncio
from playwright.async_api import async_playwright

async def bulk_screenshots(urls: list[str], output_dir: str = '.') -> None:
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)

        async def capture(url: str, idx: int) -> None:
            context = await browser.new_context(viewport={'width': 1280, 'height': 800})
            page = await context.new_page()
            try:
                await page.goto(url, wait_until='networkidle', timeout=30000)
                await page.screenshot(
                    path=f'{output_dir}/{idx:03d}.png',
                    full_page=True
                )
                print(f'✅ {url}')
            except Exception as e:
                print(f'❌ {url}: {e}')
            finally:
                await context.close()

        # Run 5 concurrent captures
        semaphore = asyncio.Semaphore(5)
        async def bounded(url, idx):
            async with semaphore:
                return await capture(url, idx)

        await asyncio.gather(*[bounded(url, i) for i, url in enumerate(urls)])
        await browser.close()

urls = ['https://example.com', 'https://news.ycombinator.com', 'https://github.com']
asyncio.run(bulk_screenshots(urls, output_dir='/tmp/screenshots'))

Method 3: REST API (No Browser)

For production workloads — especially serverless functions, Lambda, or Docker containers where binary size matters — a screenshot REST API removes all headless browser management.

import requests
import base64
import os

SNAPAPI_KEY = os.environ.get('SNAPAPI_KEY')

def screenshot_url(url: str, output_path: str, **kwargs) -> bytes:
    """Capture a URL as a PNG screenshot via SnapAPI."""
    payload = {
        'url': url,
        'full_page': kwargs.get('full_page', True),
        'width': kwargs.get('width', 1280),
        'height': kwargs.get('height', 800),
        'wait_for': kwargs.get('wait_for', 'networkidle'),
        'block_ads': kwargs.get('block_ads', True),
        'format': kwargs.get('format', 'png'),
    }
    if kwargs.get('device'):
        payload['device'] = kwargs['device']
    if kwargs.get('stealth'):
        payload['stealth'] = True

    response = requests.post(
        'https://api.snapapi.pics/v1/screenshot',
        headers={'X-Api-Key': SNAPAPI_KEY},
        json=payload,
        timeout=60
    )
    response.raise_for_status()
    data = response.json()

    image_bytes = base64.b64decode(data['screenshot'])
    with open(output_path, 'wb') as f:
        f.write(image_bytes)
    return image_bytes

# Full-page PNG
screenshot_url('https://example.com', 'full.png')

# Mobile (iPhone 14)
screenshot_url('https://example.com', 'mobile.png', device='iPhone 14')

# Stealth mode (Cloudflare-protected sites)
screenshot_url('https://protected-site.com', 'stealth.png', stealth=True)

# JPEG for smaller file size
screenshot_url('https://example.com', 'thumb.jpg', format='jpeg', full_page=False)

Async Version with httpx

import asyncio
import httpx
import base64
import os

SNAPAPI_KEY = os.environ.get('SNAPAPI_KEY')

async def screenshot_async(url: str, output_path: str) -> bytes:
    async with httpx.AsyncClient(timeout=60) as client:
        response = await client.post(
            'https://api.snapapi.pics/v1/screenshot',
            headers={'X-Api-Key': SNAPAPI_KEY},
            json={'url': url, 'full_page': True, 'wait_for': 'networkidle'}
        )
        response.raise_for_status()
        image_bytes = base64.b64decode(response.json()['screenshot'])
        with open(output_path, 'wb') as f:
            f.write(image_bytes)
        return image_bytes

asyncio.run(screenshot_async('https://example.com', 'output.png'))

Method Comparison

MethodSetup timeSPA supportAnti-botWorks on LambdaCost
SeleniumHeavy⚠️ sleep()PartialFree
PlaywrightMedium✅ networkidlePartial⚠️ layerFree
SnapAPI RESTMinimal✅ networkidle✅ stealth$0.0016/call

Django/Flask Integration

# views.py — screenshot endpoint
from django.http import HttpResponse, JsonResponse
import requests, base64, os

def screenshot_view(request):
    url = request.GET.get('url')
    if not url:
        return JsonResponse({'error': 'url parameter required'}, status=400)

    response = requests.post(
        'https://api.snapapi.pics/v1/screenshot',
        headers={'X-Api-Key': os.environ['SNAPAPI_KEY']},
        json={'url': url, 'full_page': True, 'wait_for': 'networkidle'}
    )
    data = response.json()
    image_bytes = base64.b64decode(data['screenshot'])

    return HttpResponse(image_bytes, content_type='image/png')

Background Tasks with Celery

# tasks.py
from celery import shared_task
import requests, base64, os

@shared_task(bind=True, max_retries=3)
def capture_screenshot(self, url: str, output_key: str):
    """Async screenshot capture — retries on failure."""
    try:
        response = requests.post(
            'https://api.snapapi.pics/v1/screenshot',
            headers={'X-Api-Key': os.environ['SNAPAPI_KEY']},
            json={'url': url, 'full_page': True, 'wait_for': 'networkidle'},
            timeout=60
        )
        response.raise_for_status()
        image_bytes = base64.b64decode(response.json()['screenshot'])

        # Upload to S3, save to disk, etc.
        with open(f'/tmp/{output_key}.png', 'wb') as f:
            f.write(image_bytes)
        return {'status': 'ok', 'key': output_key}

    except Exception as exc:
        raise self.retry(exc=exc, countdown=30)

# Trigger from a view:
# capture_screenshot.delay('https://example.com', 'my-screenshot')
Python quickstart: pip install requests, get your free key at snapapi.pics, and you're taking screenshots in 5 minutes. No Chromium binary. No WebDriver. No Docker.

Which Method Should You Use?

For scripts and one-off captures: Playwright is the best self-hosted option — wait_until='networkidle' handles SPAs properly and it's faster than Selenium. For production Django/Flask/FastAPI services: SnapAPI REST removes all browser maintenance burden and works out of the box on any Python hosting (Lambda, Heroku, Railway, VPS). For Celery background tasks: SnapAPI + @shared_task is the cleanest pattern — no browser process to manage per worker.