How to Save a Webpage as an Image Using Python
Use Playwright to render any webpage and save a viewport, full-page, or element screenshot in Python, with options for format, quality, and timing.
Direct answer: use Playwright for Python to open the page in a real browser, then call page.screenshot(). Use full_page=True for the entire scrollable document, pass path to save directly to disk, or omit it to receive image bytes in memory. Playwright documents PNG, JPEG, and WebP output, locator screenshots for individual elements, animation control, transparency, scale, quality, and screenshot timeouts in its Python screenshot guide and Page API reference.
Quick start: save a full webpage screenshot
Install Playwright and its browser binaries:
python -m pip install playwright
python -m playwright install chromium
Create save_page.py:
from pathlib import Path
from playwright.sync_api import sync_playwright
url = 'https://example.com'
out = Path('example.png')
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={'width': 1440, 'height': 900})
page.goto(url, wait_until='networkidle')
page.screenshot(path=str(out), full_page=True)
browser.close()
print(f'Saved {out}')
Run it with python save_page.py. The ordinary call captures the current viewport; full_page=True captures the full scrollable page. A navigation wait is only a starting point: pages that load data after navigation may need a selector wait or a short delay before capture.
Choose the capture you need
Viewport screenshot
page.screenshot(path='viewport.png')
This saves what is visible in the current viewport. Set the viewport when consistent dimensions matter:
page = browser.new_page(viewport={'width': 1280, 'height': 800})
Full-page screenshot
page.screenshot(path='entire-page.png', full_page=True)
Full-page mode creates a tall image containing the document’s scrollable content. Very long pages can produce large files or exceed downstream image limits, so consider capturing sections or resizing after capture.
One element
card = page.locator('main article').first
card.screenshot(path='article.png')
Locator screenshots are useful for a chart, header, card, or other component. Use a selector that identifies the intended element after the page has rendered.
Save bytes instead of a file
image_bytes = page.screenshot(full_page=True)
with open('page.webp', 'wb') as f:
f.write(image_bytes)
Without path, Playwright returns bytes. You can send those bytes to object storage, an image processor, or an HTTP response without creating an intermediate file.
Formats, quality, scale, and transparency
The output type is inferred from the filename extension; PNG is the default. Playwright documents PNG, JPEG, and WebP. JPEG and WebP support a quality setting; PNG does not.
page.screenshot(path='page.jpg', type='jpeg', quality=85)
page.screenshot(path='page.webp', type='webp', quality=80)
Use scale='css' for dimensions based on CSS pixels, or scale='device' for higher device-pixel dimensions. The device scale is useful for retina output but increases memory and file size.
page.screenshot(path='retina.png', scale='device')
page.screenshot(path='compact.png', scale='css')
To preserve transparency, omit the default background:
page.screenshot(path='transparent.png', omit_background=True)
Transparency does not apply to JPEG. Choose PNG or WebP when an alpha channel is required.
Wait for the page that you actually want to capture
No single readiness condition works for every site. Choose the condition that matches the page:
page.goto(url, wait_until='domcontentloaded')
page.locator('[data-loaded="true"]').wait_for()
page.screenshot(path='ready.png')
For a known delay, use a short timeout:
page.goto(url)
page.wait_for_timeout(1500)
page.screenshot(path='delayed.png')
Wait for a specific network response when client-side data drives the page:
page.goto(url)
page.wait_for_response(lambda response: '/api/products' in response.url)
page.screenshot(path='products.png')
Animations can make repeated captures differ. Disable them during the screenshot when visual consistency matters:
page.screenshot(path='stable.png', animations='disabled')
The documented screenshot timeout default is 30 seconds. Set a larger value for slow pages or a smaller one for batch jobs:
page.screenshot(path='slow-page.png', timeout=60_000)
Reusable Python functions
Synchronous helper
from pathlib import Path
from playwright.sync_api import sync_playwright
def save_webpage(url: str, output: str, full_page: bool = True) -> None:
with sync_playwright() as p:
browser = p.chromium.launch()
try:
page = browser.new_page(viewport={'width': 1440, 'height': 900})
page.goto(url, wait_until='networkidle')
page.screenshot(path=output, full_page=full_page, animations='disabled')
finally:
browser.close()
save_webpage('https://example.com', 'example.png')
Asynchronous helper
import asyncio
from playwright.async_api import async_playwright
async def save_webpage(url: str, output: str) -> None:
async with async_playwright() as p:
browser = await p.chromium.launch()
try:
page = await browser.new_page(viewport={'width': 1440, 'height': 900})
await page.goto(url, wait_until='networkidle')
await page.screenshot(path=output, full_page=True, animations='disabled')
finally:
await browser.close()
asyncio.run(save_webpage('https://example.com', 'example.png'))
Useful browser and page settings
| Need | Setting or technique |
|---|---|
| Desktop dimensions | browser.new_page(viewport={'width': 1440, 'height': 900}) |
| Mobile-like capture | Use a smaller viewport; choose a device preset when your project requires one. |
| Whole document | full_page=True |
| Specific component | page.locator(selector).screenshot() |
| Small output | JPEG/WebP with an appropriate quality value and scale='css' |
| Transparent background | omit_background=True with PNG or WebP |
| Stable animation state | animations='disabled' |
| Slow rendering | Increase timeout and wait for the page’s real readiness signal |
Common errors and fixes
| Error or symptom | Likely cause | Fix |
|---|---|---|
Executable doesn't exist |
Browser binaries were not installed. | Run python -m playwright install chromium. |
| Screenshot shows a loading spinner | The page’s data arrives after navigation. | Wait for a target locator, response, or a measured delay before calling screenshot. |
| Only the visible area is saved | The capture used default viewport mode. | Pass full_page=True. |
| Element not found | The selector is wrong or the element has not rendered. | Verify the selector and call locator.wait_for() before the element screenshot. |
| Timeout during capture | Slow page, blocked resource, or an element that never appears. | Increase the screenshot timeout, use a narrower readiness condition, and inspect the page independently. |
| Blank or partially styled image | Capture happened before fonts, styles, or scripts finished. | Wait for the relevant selector or response; avoid assuming that networkidle means visual readiness for every site. |
| Huge output file | Full-page and device-pixel scale multiply image dimensions. | Use CSS scale, JPEG/WebP quality, element captures, or post-process the bytes. |
| Different result on each run | Animations, rotating content, ads, or time-dependent data. | Disable animations, set a fixed viewport, and control waits and inputs where possible. |
Performance, reliability, and cost considerations
- Launching a browser is expensive compared with reusing one. For batches, keep one browser process open and create or reuse pages carefully.
- Full-page captures require more memory than viewport or element captures. Limit concurrency when pages are long or media-heavy.
- Use WebP or JPEG when your consumer does not require lossless PNG or transparency.
- Set explicit viewport, locale, timezone, and waits when reproducibility matters. Dynamic ads and third-party scripts can still change output.
- Close pages and browsers in
finallyblocks so failures do not leak processes. - For private or authenticated pages, provide the required browser context credentials or cookies and avoid logging secrets.
Or skip the browser setup
ScreenshotNeo provides a hosted screenshot API. One GET request returns a PNG, JPEG, WebP, or PDF. Cookie and consent banners are accepted and removed before capture, along with 60+ known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. An MCP server also lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf.
See the ScreenshotNeo API documentation for the complete option list. Basic call:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get('https://api.screenshotneo.com/v1/shot', params={'access_key': 'YOUR_API_KEY', 'url': 'https://stripe.com'}, timeout=90)
open('shot.webp', 'wb').write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo includes full-page and element capture, dark mode, device presets, retina scale, custom CSS and JavaScript, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, resizing, caching, signed links, asynchronous jobs, webhooks, bulk capture for up to 100 URLs per call, a usage API, and an OpenAPI specification. Every feature is on every plan. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.
FAQ
Can Python save a screenshot without writing a temporary file?
Yes. Omit path from page.screenshot(); Playwright returns bytes that you can process or upload directly.
What is the difference between a viewport and a full-page screenshot?
A viewport screenshot contains the currently visible area. A full-page screenshot includes the page’s full scrollable document.
Which format should I choose?
Use PNG for lossless output and transparency, JPEG for smaller photographic images, and WebP when you want modern compression with quality control.
Why does a screenshot differ from what I see in my desktop browser?
Viewport size, device scale, fonts, animations, login state, cookies, geolocation, third-party content, and page timing can all differ. Set the relevant inputs explicitly and wait for a deterministic page state.


