ScreenshotNeo

BlogHow-to

How to Use Playwright with Python: A Free Tutorial

Install Playwright, automate Chromium, write pytest tests, choose sync or async APIs, and troubleshoot browser setup with runnable Python examples.

By the ScreenshotNeo team1 October 20269 min read

Playwright lets Python code control Chromium, Firefox and WebKit for browser automation and end-to-end testing. Install the Python package, install the matching browser binaries, then choose either the standalone library API for scripts or the official pytest plugin for repeatable test suites.

This tutorial covers installation, synchronous and asynchronous APIs, locators, screenshots, pytest fixtures, code generation, browser selection, CI, troubleshooting, performance and cost considerations.

1. Choose your Playwright setup

Use case Recommended setup Why
One-off automation script playwright library Direct control over browser, context and page objects.
End-to-end test suite pytest-playwright Fixtures, browser configuration and a repeatable pytest workflow. Playwright recommends the official Playwright Pytest plugin for end-to-end tests (official installation guide).

2. Install Playwright and browser binaries

Standalone library

python -m venv .venv
# macOS/Linux
source .venv/bin/activate
# Windows PowerShell
# .venv\Scripts\Activate.ps1

python -m pip install --upgrade pip
pip install playwright
playwright install

Pytest plugin

python -m venv .venv
source .venv/bin/activate
pip install pytest-playwright
playwright install

The package and browser binaries are separate. Run playwright install after installing the package and again when a Playwright upgrade requires newer browser revisions. You can install only selected engines, for example playwright install chromium.

Poetry and uv

# Poetry
poetry add playwright
poetry run playwright install

# uv
uv add playwright
uv run playwright install

Check the current requirements on the official installation page before publishing a pinned setup. The documented requirements include Python 3.8 or newer and supported, version-sensitive operating-system combinations.

3. Your first synchronous Python script

The synchronous API is the simplest starting point for a command-line script.

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page(viewport={"width": 1280, "height": 800})
    page.goto("https://example.com", wait_until="domcontentloaded")
    print(page.title())
    page.screenshot(path="example.png", full_page=True)
    browser.close()

browser.new_page() creates an isolated browser context and page for a small example. In a larger program, create a context explicitly so you can set permissions, locale, timezone, cookies or viewport once and reuse it across pages.

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    context = browser.new_context(
        viewport={"width": 1440, "height": 900},
        locale="en-US",
        timezone_id="America/New_York",
    )
    page = context.new_page()
    page.goto("https://example.com", wait_until="networkidle")
    print(page.locator("h1").inner_text())
    context.close()
    browser.close()

4. The asynchronous API

Use async_playwright() when your application already uses asyncio, async web frameworks or concurrent browser tasks. Do not call the synchronous API from an active event loop.

import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()
        await page.goto("https://example.com", wait_until="domcontentloaded")
        print(await page.title())
        await page.screenshot(path="example-async.png", full_page=True)
        await browser.close()

if __name__ == "__main__":
    asyncio.run(main())

5. Write a pytest end-to-end test

The pytest plugin supplies a page fixture and integrates browser startup with pytest.

# tests/test_homepage.py
from playwright.sync_api import Page, expect

def test_homepage(page: Page):
    page.goto("https://example.com")
    expect(page).to_have_title("Example Domain")
    expect(page.locator("h1")).to_have_text("Example Domain")
pytest

Tests normally start with test_. Use web-first assertions such as expect(locator).to_be_visible() instead of reading a value once and asserting immediately; Playwright retries the assertion while the page reaches the expected state.

Useful pytest commands

# Run one file
pytest tests/test_homepage.py

# Run headed so you can watch the browser
pytest --headed

# Choose a browser project
pytest --browser chromium
pytest --browser firefox
pytest --browser webkit

# Keep traces for failed tests (when supported by your plugin setup)
pytest --tracing retain-on-failure

6. Locators and resilient interactions

Prefer locators that describe how a user identifies an element. Role locators are usually clearer and less fragile than long CSS or XPath selectors.

from playwright.sync_api import Page, expect

def test_sign_in(page: Page):
    page.goto("https://example.com/login")
    page.get_by_role("textbox", name="Email").fill("user@example.com")
    page.get_by_label("Password").fill("correct-horse-battery-staple")
    page.get_by_role("button", name="Sign in").click()
    expect(page.get_by_role("heading", name="Dashboard")).to_be_visible()

Other useful choices include get_by_text(), get_by_label(), get_by_placeholder() and get_by_test_id(). Add stable test IDs to your application when accessible roles or labels are not unique. Avoid selectors tied to generated class names or DOM nesting.

Waiting correctly

# Wait for a specific state
page.get_by_role("button", name="Load report").click()
expect(page.get_by_role("status")).to_have_text("Ready")

# Wait for navigation caused by an action
with page.expect_navigation():
    page.get_by_role("link", name="Account").click()

# Wait for a request when the response itself matters
with page.expect_response("**/api/orders") as response_info:
    page.get_by_role("button", name="Refresh").click()
response = response_info.value
assert response.ok

Use a targeted wait for a selector, URL, navigation or response. Fixed sleeps slow tests and still fail when the page needs longer than the chosen delay.

7. Record a first draft with Codegen

Codegen opens a browser, records actions and suggests locators prioritized around roles, text and test IDs. Treat generated code as a starting point: remove unnecessary steps, give tests meaningful names and replace unstable selectors.

playwright codegen https://example.com

See the official Codegen guide for recording options and locator behavior.

8. Browser, context and page configuration

Chromium, Firefox and WebKit

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    for browser_type in (p.chromium, p.firefox, p.webkit):
        browser = browser_type.launch()
        page = browser.new_page()
        page.goto("https://example.com")
        print(browser_type.name, page.title())
        browser.close()

Playwright also supports selected branded browser channels. Browser binaries are tied to Playwright releases; consult the browser documentation when upgrading or selecting a channel.

Common context options

context = browser.new_context(
    viewport={"width": 1280, "height": 720},
    device_scale_factor=2,
    color_scheme="dark",
    locale="en-GB",
    timezone_id="Europe/London",
    user_agent="my-test-agent/1.0",
    extra_http_headers={"X-Test-Run": "playwright"},
    ignore_https_errors=False,
)

Use a context for isolation: each test can have separate cookies, storage and permissions without launching another browser process.

Cookies and authenticated state

context = browser.new_context(
    storage_state="auth.json"
)

# Save state after logging in
context.storage_state(path="auth.json")

Keep authentication files out of source control. Create a fresh state for CI or store it in the CI secret system.

9. Screenshots, PDFs and downloads

# Element screenshot
page.locator("article").screenshot(path="article.png")

# Full page screenshot
page.screenshot(path="full-page.png", full_page=True)

# PDF is supported by Chromium
page.pdf(path="report.pdf", format="A4", print_background=True)

# Download
with page.expect_download() as download_info:
    page.get_by_role("link", name="Export CSV").click()
download = download_info.value
download.save_as("report.csv")

10. Network control and test isolation

Route requests to stub unstable dependencies, block analytics, or inspect API traffic.

def mock_orders(route):
    route.fulfill(
        status=200,
        content_type="application/json",
        body='{"orders": []}',
    )

context.route("**/api/orders", mock_orders)
page.goto("https://example.com/orders")

Use mocking for deterministic unit-like browser tests, then keep a smaller set of tests against real services to catch integration failures.

11. Continuous integration

CI needs both the Python dependencies and Playwright browser binaries. A typical setup is:

python -m pip install -r requirements.txt
python -m playwright install --with-deps
pytest

The --with-deps option installs Linux system dependencies where supported. Follow the current official CI guidance for your runner image. Save traces, screenshots and videos as CI artifacts when a failure needs inspection.

12. Troubleshooting

Error or symptom Likely cause Fix
Executable doesn't exist Browser binaries were not installed or no longer match the package. Run playwright install with the same environment that runs tests.
Browser fails to start in Linux CI Missing shared libraries or sandbox dependencies. Use a supported runner image and playwright install --with-deps; check the CI guide.
Timeout waiting for locator Wrong locator, delayed rendering, iframe or a blocked request. Verify the locator in headed mode, wait for a meaningful state, and use frame_locator() for iframes.
Click intercepted Another element overlays the target or an animation is active. Prefer a visible, user-facing locator; wait for it to be actionable and remove test-only overlays.
Test passes locally but fails in CI Different viewport, browser version, timezone, data or timing. Set context options explicitly, avoid sleeps, isolate test data and retain a trace on failure.
Async API raises event-loop errors Synchronous API was called from asyncio, or coroutines were not awaited. Use async_playwright() throughout the async call chain and await every Playwright operation.
Tests affect one another Shared cookies, storage or mutable backend data. Create isolated contexts, reset test data and avoid global page objects.

13. Performance, reliability and cost

  • Launch one browser per worker and create contexts per test when possible; browser startup is more expensive than opening a page.
  • Reuse a context for related steps, but keep tests isolated enough to prevent state leakage.
  • Run only the engines you need locally, then use a cross-browser matrix in CI.
  • Use targeted waits and API mocking to reduce idle time and external-service flakiness.
  • Pin dependencies in CI and rerun browser installation after upgrades so package and binaries stay aligned.
  • Playwright itself is open-source software; budget for CI minutes, browser downloads, test environments and any external services your tests call.

14. Or skip the browser setup

If your goal is to obtain a clean screenshot rather than interact with a browser, ScreenshotNeo provides a website screenshot API. One GET request returns PNG, JPEG, WebP or PDF. Cookie and consent banners, newsletter popups and chat widgets are removed before capture; bot checks, blank pages, failed loads, timeouts and cache hits are not billed, and response headers identify the page verdict and billing result.

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(`HTTP ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', image);

ScreenshotNeo also includes an MCP server with take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients. It supports full-page and element captures, custom CSS and JavaScript, device presets, dark mode, waits, headers, cookies, geolocation, blocking rules, caching, signed links, asynchronous jobs, bulk capture and a usage API. 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 each month with no card.

15. FAQ

Should I use Playwright sync or async in a Django or FastAPI project?

Use the async API when the surrounding request handling and job system are asyncio-based. Keep the sync API for isolated scripts or synchronous workers.

Can Playwright test Safari?

Playwright’s WebKit engine is intended for browser-engine coverage. It is not the same as automating an installed Safari application.

Do I need pytest?

No. The standalone library works without pytest. Pytest adds fixtures and test-runner structure for end-to-end suites.

Why does a Playwright upgrade require another install command?

Playwright releases are paired with browser revisions. Installing the new package does not always install those binaries automatically, so run playwright install.

When is ScreenshotNeo a better fit?

Use it when you need a screenshot or PDF endpoint without maintaining browser processes, consent-banner handling and capture cleanup in your own code.

Sources