ScreenshotNeo

BlogHow-to

How to Take Full-Page Screenshots in Python

Use Playwright’s full_page=True to capture an entire scrollable webpage in Python, with reliable waits, formats, troubleshooting, and an API shortcut.

By the ScreenshotNeo team29 September 20268 min read

How to Take Full-Page Screenshots in Python

Direct answer: use Playwright for Python and pass full_page=True to page.screenshot(). Playwright captures the complete scrollable page as one image instead of only the visible viewport. The option works in both synchronous and asynchronous Python code.

page.screenshot(path="screenshot.png", full_page=True)

The rest of this guide shows a complete setup, production-friendly waits, output options, dynamic-page techniques, troubleshooting, and an API alternative when you do not want to manage a browser.

1. Install Playwright and its browser

Install the Python package, then download a browser engine:

python -m pip install playwright
python -m playwright install chromium

On Linux CI systems, Playwright may also need operating-system libraries. The install command can request them when your environment allows it:

python -m playwright install --with-deps chromium

Pin Playwright in your project requirements so that browser behavior is repeatable across deployments.

2. Minimal synchronous example

The synchronous API is convenient for scripts and command-line jobs. It launches Chromium, opens a page, navigates to the target, and writes a full-page PNG:

A full-page capture renders the complete scrollable document in one screenshot operation.
A full-page capture renders the complete scrollable document in one screenshot operation.
from playwright.sync_api import sync_playwright

URL = "https://example.com"

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto(URL)
    page.screenshot(path="screenshot.png", full_page=True)
    browser.close()

full_page=True is the key setting. Without it, Playwright captures only the current viewport. The official guide defines a full-page screenshot as the full scrollable page rendered as if it fit on one very tall screen. See the Playwright screenshots guide.

3. Asynchronous Python

Use the async API when your application already runs an event loop or captures many pages concurrently:

import asyncio
from playwright.async_api import async_playwright

URL = "https://example.com"

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()
        await page.goto(URL)
        await page.screenshot(path="screenshot.png", full_page=True)
        await browser.close()

asyncio.run(main())

Do not mix synchronous Playwright calls into an async event loop. Keep the browser and page objects inside the same API style.

4. Make captures deterministic

A navigation response does not always mean that the page is visually complete. Single-page applications, fonts, images, and client-side data may continue loading. Add an explicit navigation policy and a targeted readiness check:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page(viewport={"width": 1440, "height": 900})
    page.goto(
        "https://example.com",
        wait_until="networkidle",
        timeout=60_000,
    )
    page.locator("main").wait_for(state="visible", timeout=30_000)
    page.screenshot(
        path="page.png",
        full_page=True,
        animations="disabled",
        timeout=60_000,
    )
    browser.close()

Use domcontentloaded for faster captures when the page is static. Use networkidle cautiously: analytics, advertisements, or long polling can keep the network busy. A selector that represents the page’s actual content is often a better readiness signal.

Lazy-loaded content

Full-page capture does not guarantee that every lazy image has already been fetched. Scroll through the document before taking the shot, then return to the top:

page.goto("https://example.com", wait_until="domcontentloaded")
page.locator("main").wait_for(state="visible")
page.evaluate("""async () => {
  await new Promise((resolve) => {
    const distance = 700;
    const delay = 100;
    let total = 0;
    const timer = setInterval(() => {
      window.scrollBy(0, distance);
      total += distance;
      if (total >= document.body.scrollHeight) {
        clearInterval(timer);
        window.scrollTo(0, 0);
        setTimeout(resolve, 500);
      }
    }, delay);
  });
}""")
page.screenshot(path="lazy-loaded.png", full_page=True)

For sites that expose a stable image or content selector, wait for that selector instead of relying only on a timer.

5. Screenshot options you will use most

Option Purpose Notes
path Save the image to a file If omitted, the method returns bytes.
full_page Capture the complete scrollable document Defaults to False; set it explicitly.
type Choose PNG, JPEG, or WebP PNG is lossless; JPEG and WebP can reduce size.
quality Control JPEG/WebP compression It does not apply to PNG.
scale Choose CSS pixels or device pixels css gives one image pixel per CSS pixel; device follows device pixel ratio.
clip Capture a rectangular region Useful for a known area; it is separate from full-page capture.
animations Control CSS and Web Animations Use "disabled" for repeatable output.
caret Show or hide the text caret Hide it for visual diffs.
timeout Set the capture timeout Increase it for very tall or image-heavy pages.
mask Cover selected locators Useful for changing timestamps or personal data.
style Inject a stylesheet during capture Available in recent Playwright versions; check your installed API version.

The exact defaults and type rules are documented in the Page API reference.

6. Choose output format and capture bytes

When you need to upload directly to object storage, omit path and receive image bytes:

from pathlib import Path
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://example.com", wait_until="domcontentloaded")
    data = page.screenshot(
        type="webp",
        quality= eighty if False else 82,
        full_page=True,
    )
    Path("page.webp").write_bytes(data)
    browser.close()

Replace the illustrative expression above with a normal integer in real code:

data = page.screenshot(type="webp", quality=82, full_page=True)

JPEG and WebP quality values trade file size against visual fidelity. Use PNG when exact pixels, transparency, or lossless archival output matters.

7. Viewport, device scale, and responsive layouts

The same URL can render differently at different viewport sizes. Set the viewport explicitly for reproducible captures:

context = browser.new_context(
    viewport={"width": 1280, "height": 800},
    device_scale_factor=1,
)
page = context.new_page()

Use device_scale_factor=2 for a retina-style context. This increases image dimensions and memory use. If your downstream system expects CSS-pixel dimensions, use scale="css" in the screenshot call.

8. Hide, mask, or restyle content

Transient UI can make captures inconsistent. You can inject CSS to hide a selector, or mask a locator when the region should remain present but its value changes:

page.add_style_tag(content="""
  .cookie-banner, .newsletter-modal, .chat-widget {
    display: none !important;
  }
""")
page.screenshot(
    path="clean.png",
    full_page=True,
    mask=[page.locator(".live-price")],
    mask_color="#888888",
)

Only hide elements when doing so matches your capture goal. If you need a visitor-accurate screenshot, preserve the page’s own UI and handle consent explicitly.

9. Common errors and fixes

Error or symptom Likely cause Fix
Executable doesn't exist The browser binary was not installed. Run python -m playwright install chromium.
Only the visible area is captured full_page was omitted or false. Pass full_page=True.
Images are missing Lazy loading has not been triggered. Scroll through the page, wait for image selectors, then capture.
Capture times out Network idle never occurs, or the page is unusually large. Use a targeted selector, choose domcontentloaded, and increase timeout.
Animations produce different pixels CSS transitions or Web Animations are still running. Set animations="disabled" and wait for the final state.
Text or layout differs between machines Viewport, device scale, fonts, timezone, or browser versions differ. Pin Playwright, set the viewport, and standardize the runtime.
Very tall pages consume too much memory A full document is one large bitmap. Use a smaller scale, a compressed format, element screenshots, or split the page into sections.
Login page appears instead of content The page requires authentication. Create a browser context with the required storage state, cookies, or headers.
Cookie dialog covers the page Consent has not been handled. Click the site’s consent control, inject a narrowly scoped style, or use a service that handles common consent UI.

10. Reliability and performance practices

  • Reuse the browser: launch Chromium once per worker and create separate contexts for jobs.
  • Limit concurrency: each full-page bitmap uses memory proportional to its pixel dimensions. Start with a small worker pool and increase it only after observing memory pressure.
  • Set timeouts: use navigation and screenshot timeouts so one broken site cannot hold a worker forever.
  • Retry selectively: retry transient network failures, but record the URL and exception. Repeating a deterministic selector or authentication error will not help.
  • Capture diagnostics: log the final URL, viewport, browser version, and readiness selector. Save a trace or HTML only when debugging because these artifacts can contain sensitive data.
  • Control page growth: pages with infinite feeds may never have a stable full height. Capture a defined section or stop scrolling at a documented limit.
  • Check output: verify that the file exists, has nonzero bytes, and can be decoded before publishing it.

11. Security and privacy considerations

Browser automation can load third-party resources and authenticated data. Run untrusted targets in an isolated environment, avoid printing cookies or authorization headers to logs, and treat screenshots as potentially sensitive files. If you inject custom JavaScript or CSS, keep it narrowly scoped to the capture job.

12. When an API is simpler

Managing browser binaries, concurrency, consent dialogs, bot checks, and failed pages is useful when you need complete browser control. For scheduled captures or backend workflows, a screenshot API can remove that operational work.

Consent and transient overlays can be handled before producing a clean capture.
Consent and transient overlays can be handled before producing a clean capture.

Or skip the browser setup

ScreenshotNeo returns a PNG, JPEG, WebP, or PDF from one GET request. Its API accepts full-page capture and the other parameters used by screenshot services, while its browser workflow handles common capture problems before billing.

See the ScreenshotNeo API documentation for all options.

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,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const buffer = Buffer.from(await res.arrayBuffer());
  • Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot.
  • Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers identify the page verdict and billing state.
  • An MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf.
  • Every plan includes the features. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots.

Create a free ScreenshotNeo account and start with 1,000 screenshots a month without adding a card.

13. FAQ

Does full-page capture stitch multiple screenshots?

Playwright exposes it as one page.screenshot() operation. The browser captures the full scrollable document for you; you do not need to manually calculate viewport offsets.

Can I capture a single element instead?

Yes. Locate the element and call its screenshot method when you need a component rather than the entire document.

Should I use PNG or WebP?

Choose PNG for lossless output and transparency. Choose WebP or JPEG when smaller files matter and some compression is acceptable.

Why is my page height different on each run?

Dynamic content, fonts, ads, animations, viewport settings, and lazy loading can all change layout. Fix the viewport, wait for a stable selector, disable animations, and control or remove variable content.

Can this run in a serverless function?

It can, provided the runtime supports the required browser binary and dependencies. Package size, startup time, memory limits, and sandbox restrictions are deployment constraints to evaluate for your platform.

How do I capture a PDF instead of an image?

Chromium’s PDF workflow is separate from page.screenshot(). If you need paper size, margins, landscape mode, or page ranges through an API, ScreenshotNeo’s capture_pdf tool and PDF options are available on every plan.