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')
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
| Method | Setup time | SPA support | Anti-bot | Works on Lambda | Cost |
|---|---|---|---|---|---|
| Selenium | Heavy | ⚠️ sleep() | Partial | ❌ | Free |
| Playwright | Medium | ✅ networkidle | Partial | ⚠️ layer | Free |
| SnapAPI REST | Minimal | ✅ 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')
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.