Taking a screenshot of a webpage sounds trivial. But there’s a gap between “screenshot of what’s visible” and “screenshot of the entire page including everything below the fold.”

Playwright handles both, plus a lot more — specific elements, PDFs, custom viewports, and screenshots across hundreds of pages in parallel. Here’s how all of it works.


Requirements

  • Python 3.8+
  • Playwright installed
pip install playwright
playwright install chromium

Basic screenshot

The simplest case — screenshot of what’s visible in the viewport:

import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page()

        await page.goto('https://example.com')
        await page.screenshot(path='screenshot.png')

        await browser.close()

asyncio.run(main())

This saves a PNG of whatever fits in the default viewport (1280×720).


Full-page screenshot

Add full_page=True and Playwright scrolls through the entire page, stitches it together, and saves one tall image.

import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page()

        await page.goto('https://example.com')
        await page.screenshot(path='full-page.png', full_page=True)

        await browser.close()

asyncio.run(main())

That’s it. One parameter change and you get the full page.


Setting the viewport size

The viewport controls how wide the page renders. Default is 1280×720. Change it to match whatever resolution you need.

import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)

        # Desktop
        page = await browser.new_page(viewport={'width': 1920, 'height': 1080})
        await page.goto('https://example.com')
        await page.screenshot(path='desktop.png', full_page=True)

        # Mobile
        page_mobile = await browser.new_page(viewport={'width': 390, 'height': 844})
        await page_mobile.goto('https://example.com')
        await page_mobile.screenshot(path='mobile.png', full_page=True)

        await browser.close()

asyncio.run(main())

Useful for checking how a page looks across different devices without opening a browser manually.


Screenshot a specific element

Instead of the full page, you can screenshot just one element. Pass a locator to .screenshot().

import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page()

        await page.goto('https://example.com/pricing')

        # Screenshot just the pricing table
        pricing_table = page.locator('.pricing-table')
        await pricing_table.wait_for()
        await pricing_table.screenshot(path='pricing.png')

        await browser.close()

asyncio.run(main())

This is handy for visual regression testing — compare just the component you changed, not the whole page.


Save as JPEG instead of PNG

PNG files are large. For screenshots where exact color accuracy doesn’t matter, JPEG is significantly smaller.

await page.screenshot(
    path='screenshot.jpg',
    type='jpeg',
    quality=85,  # 0-100, higher = better quality + larger file
    full_page=True
)

For reference: a full-page PNG of a typical site might be 2–4 MB. The same page as JPEG at quality 85 is usually 200–500 KB.


Get screenshot as bytes (no file)

If you’re storing screenshots in a database or sending them somewhere without touching the filesystem:

import asyncio
import base64
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page()

        await page.goto('https://example.com')

        # Returns bytes directly
        screenshot_bytes = await page.screenshot(full_page=True)

        # Example: encode as base64 for an API
        encoded = base64.b64encode(screenshot_bytes).decode('utf-8')
        print(f'Screenshot size: {len(screenshot_bytes)} bytes')

        await browser.close()

asyncio.run(main())

Wait for content before screenshotting

Here’s the thing with dynamic pages: if you screenshot immediately after goto(), you might catch the page mid-load. JavaScript content, lazy-loaded images, animations — none of it is done yet.

import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page()

        await page.goto('https://example.com/dashboard')

        # Option 1: wait for network to go idle (no requests for 500ms)
        await page.wait_for_load_state('networkidle')

        # Option 2: wait for a specific element to appear
        await page.locator('.dashboard-chart').wait_for()

        # Option 3: wait a fixed amount of time (last resort)
        await page.wait_for_timeout(2000)

        await page.screenshot(path='dashboard.png', full_page=True)
        await browser.close()

asyncio.run(main())

networkidle works well for most cases. Use element waiting when you know exactly what needs to load. Fixed delays are the least reliable — use them only when nothing else works.


Save as PDF instead of image

Playwright can also save a page as a PDF. This only works in Chromium (not Firefox or WebKit).

import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page()

        await page.goto('https://example.com/report')
        await page.wait_for_load_state('networkidle')

        await page.pdf(
            path='report.pdf',
            format='A4',
            print_background=True,  # include background colors and images
            margin={'top': '20px', 'bottom': '20px', 'left': '20px', 'right': '20px'}
        )

        await browser.close()

asyncio.run(main())

print_background=True matters a lot visually. Without it, background colors and images get stripped, which makes most pages look broken.


Screenshot multiple pages in parallel

This is where Playwright gets fast. Parallel context-per-page screenshotting:

import asyncio
import os
from playwright.async_api import async_playwright

URLS = [
    'https://example.com',
    'https://example.org',
    'https://example.net',
    'https://playwright.dev',
    'https://github.com',
]

SAVE_DIR = './screenshots'
os.makedirs(SAVE_DIR, exist_ok=True)

async def screenshot_page(browser, url: str, index: int):
    context = await browser.new_context(viewport={'width': 1440, 'height': 900})
    page = await context.new_page()

    try:
        await page.goto(url, timeout=30000)
        await page.wait_for_load_state('networkidle')

        filename = f'page_{index:03d}.png'
        await page.screenshot(
            path=os.path.join(SAVE_DIR, filename),
            full_page=True
        )
        print(f'[{index}] Saved: {filename} ({url})')
    except Exception as e:
        print(f'[{index}] Failed: {url}{e}')
    finally:
        await context.close()

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)

        tasks = [screenshot_page(browser, url, i) for i, url in enumerate(URLS)]
        await asyncio.gather(*tasks)

        await browser.close()

asyncio.run(main())

Each URL gets its own browser context so they’re fully isolated. asyncio.gather() runs them all at once. For 100 URLs this is dramatically faster than running them one by one.


Real-world use case: visual change monitoring

One practical use for automated screenshots: check whether a competitor’s pricing page changed since yesterday.

import asyncio
import os
import hashlib
from datetime import date
from playwright.async_api import async_playwright

MONITOR_URLS = {
    'competitor_pricing': 'https://competitor.com/pricing',
    'competitor_homepage': 'https://competitor.com',
}

SAVE_DIR = './monitoring'
os.makedirs(SAVE_DIR, exist_ok=True)

async def capture_and_compare(page, name: str, url: str):
    await page.goto(url)
    await page.wait_for_load_state('networkidle')

    screenshot_bytes = await page.screenshot(full_page=True)

    today = date.today().isoformat()
    today_path = os.path.join(SAVE_DIR, f'{name}_{today}.png')

    with open(today_path, 'wb') as f:
        f.write(screenshot_bytes)

    # Check if content changed vs yesterday
    today_hash = hashlib.md5(screenshot_bytes).hexdigest()
    yesterday = str(date.fromordinal(date.today().toordinal() - 1).isoformat())
    yesterday_path = os.path.join(SAVE_DIR, f'{name}_{yesterday}.png')

    if os.path.exists(yesterday_path):
        with open(yesterday_path, 'rb') as f:
            yesterday_hash = hashlib.md5(f.read()).hexdigest()

        if today_hash != yesterday_hash:
            print(f'CHANGED: {name} ({url})')
        else:
            print(f'No change: {name}')
    else:
        print(f'First capture: {name}')

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page(viewport={'width': 1440, 'height': 900})

        for name, url in MONITOR_URLS.items():
            await capture_and_compare(page, name, url)

        await browser.close()

asyncio.run(main())

Run this as a daily cron job and you’ll know immediately when a competitor changes their pricing or homepage. It’s a simple hash comparison — it tells you something changed but not what. For that you’d need a proper visual diff tool, but this is a good starting point.


Common issues

Screenshot cuts off before the bottom of the page You’re missing full_page=True. Without it, Playwright only captures the visible viewport.

Page looks different from what you see in a browser Two common reasons: the viewport is different, or the page detects headless mode and renders differently. Try viewport={'width': 1440, 'height': 900} and if that doesn’t help, add a user agent string.

Dynamic content isn’t loaded in the screenshot Wait for it first. Use page.wait_for_load_state('networkidle') or wait for a specific element with page.locator('.element').wait_for().

PDF has no background colors Add print_background=True to the page.pdf() call. Most pages look broken without it.

Screenshots are blurry on high-DPI displays Set the device scale factor: browser.new_context(device_scale_factor=2). This doubles the resolution for sharper images.