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.