Python Playwright: A Comprehensive Guide
Learn how to install, structure, debug, and scale Python Playwright automation with pytest, reliable locators, browser coverage, traces, and API testing.

Playwright for Python automates web applications in Chromium, Firefox, and WebKit. You can use its synchronous or asynchronous library directly for scripts, or use the official pytest-playwright plugin for end-to-end test suites. This guide takes you from installation to maintainable tests, including locators, waiting, browser projects, code generation, traces, API requests, reliability, and CI troubleshooting.
Choose a Python Playwright workflow
There are two sound starting points:
- Standalone library: use
playwrightwhen you need a script, scraper, migration tool, or direct control over browser contexts. - Pytest plugin: use
pytest-playwrightfor an end-to-end suite. The official documentation recommends the plugin because it supplies fixtures and built-in multi-browser configuration. See the installation guide and writing-tests guide.
The plugin gives each test an isolated browser context and page. That isolation prevents cookies, local storage, and other browser state from leaking between tests. A standalone script gives you more lifecycle control, but you must create and close contexts yourself.
Install Playwright and its browsers
The Python package and browser binaries are separate installations. Browser revisions track Playwright releases, so rerun the browser install command after upgrades when the required revision changes.
Standalone library
python -m venv .venv
# macOS/Linux
source .venv/bin/activate
# Windows PowerShell: .venv\\Scripts\\Activate.ps1
pip install playwright
playwright install
Pytest workflow
pip install pytest-playwright
playwright install
Poetry and uv can install the same packages through their normal dependency commands. Check the current requirements page for supported Python and operating-system versions before pinning a CI image.
For a reproducible project, pin the package in your dependency file and install browsers during environment setup. In containers, make sure the image includes the system dependencies required by the selected browsers.
Write your first standalone script
The synchronous API is a clear default for command-line scripts and ordinary pytest code.

from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://playwright.dev")
print(page.title())
browser.close()
browser.new_page() creates a page in a new context. For multiple independent sessions, create explicit contexts:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
admin_context = browser.new_context()
visitor_context = browser.new_context()
admin_page = admin_context.new_page()
visitor_page = visitor_context.new_page()
admin_page.goto("https://example.com/admin")
visitor_page.goto("https://example.com/")
admin_context.close()
visitor_context.close()
browser.close()
Close pages, contexts, and the browser in a predictable order, especially in long-running workers.
Use the async API when your application uses asyncio
Playwright documents both sync and async forms. Pick one model for a given module; do not mix synchronous calls into an async 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://playwright.dev")
print(await page.title())
await browser.close()
asyncio.run(main())
The async documentation warns that cancelling a task during a Playwright call has undefined behavior. Await each operation to completion and design cancellation around your own task boundaries. The Playwright API is also not thread-safe; create a separate Playwright instance per thread in multithreaded programs.
Write a first pytest test
With pytest-playwright, the built-in page fixture starts a fresh page for each test. Tests run headless by default and use Chromium unless you configure another project.
from playwright.sync_api import Page, expect
def test_get_started_link(page: Page):
page.goto("https://playwright.dev/")
page.get_by_role("link", name="Get started").click()
expect(page.get_by_role("heading", name="Installation")).to_be_visible()
Run it with:
pytest
Use --headed while developing to see the browser, and use --browser firefox or --browser webkit for a focused run. The plugin’s fixtures also expose context and browser objects when you need custom setup.
Choose locators that survive UI changes
Locators are central to Playwright’s auto-waiting and retry behavior. Prefer selectors that express how a user identifies an element:
page.get_by_role("button", name="Save")
page.get_by_label("Email")
page.get_by_placeholder("Search")
page.get_by_text("Privacy settings")
page.get_by_test_id("checkout-submit")
Use filters and chaining to narrow a region:
card = page.get_by_role("listitem").filter(
has_text="Python Playwright"
)
card.get_by_role("button", name="Open").click()
Test IDs are useful when they are an intentional contract between the application and its tests. CSS and XPath can be appropriate for a component with no accessible hook, but positional selectors such as div:nth-child(3) tend to break when layout changes.
Use strictness as feedback: if a locator matches multiple elements, refine it instead of silently selecting the first match. A locator can be reused because it resolves against the current DOM each time.
Wait for conditions, not arbitrary time
Locator actions wait for actionability, including visibility, stability, and enabled state. Web-first assertions retry until the expected condition is true or the assertion timeout expires.
from playwright.sync_api import expect
page.get_by_role("button", name="Load report").click()
expect(page.get_by_role("heading", name="Report ready")).to_be_visible()
expect(page.get_by_test_id("row-count")).to_have_text("42")
Avoid time.sleep() as a synchronization strategy. It can either waste time or inspect stale state. Prefer an assertion, a locator wait, a URL assertion, or a response wait tied to the behavior you need:
with page.expect_response("**/api/report"):
page.get_by_role("button", name="Refresh").click()
expect(page.get_by_test_id("report-table")).to_be_visible()
Use a short explicit timeout only for a known exceptional operation. Keep the default assertion timeout for normal UI behavior so failures remain informative.
Run Chromium, Firefox, and WebKit
Playwright supports all three engines. Select coverage based on the browsers your users actually run and the rendering differences your product depends on. Browser binaries are tied to Playwright releases; see the browser documentation for current channels and configuration.
pytest --browser chromium
pytest --browser firefox
pytest --browser webkit
For a matrix in CI, run separate jobs or configure pytest projects so failures identify the engine. Device emulation, timezone, locale, geolocation, color scheme, and viewport are context settings:
def test_mobile_header(page):
# Configure this through pytest projects or a custom context in your suite.
page.goto("https://example.com")
expect(page.get_by_role("banner")).to_be_visible()
Do not assume a branded browser channel or device profile is installed by default. Install and configure only what your current browser guide supports.
Generate a first draft with Codegen
Codegen opens the Inspector, records interactions, and suggests locators with a preference for roles, text, and test IDs.
playwright codegen https://playwright.dev/
Use the generated file as a draft. Replace incidental clicks with the behavior you intend to verify, remove redundant steps, and add assertions. Codegen is a locator and interaction aid, not a guarantee that the result is maintainable or complete.
Debug failures with traces
Tracing records a timeline of actions, source, logs, network activity, and DOM snapshots. For pytest, enable traces on a run:

pytest --tracing on
Use retain-on-failure when you want artifacts only for failed tests. Open the resulting trace in the Trace Viewer and inspect the action that first diverged from the expected state. Traces can contain page data and test data; handle them according to your project’s retention rules. The browser-hosted viewer loads traces locally rather than transmitting them externally, according to the official documentation.
Test APIs with APIRequestContext
Playwright can issue HTTP(S) requests without loading a page. API requests are useful for API-focused tests, preparing server-side state before a UI test, and validating a postcondition after a browser action.
from playwright.sync_api import Playwright, sync_playwright
def test_api_and_ui(p: Playwright):
request = p.request.new_context(base_url="https://api.example.com")
response = request.get("/health")
assert response.ok
request.dispose()
with sync_playwright() as p:
test_api_and_ui(p)
Keep UI coverage for user interactions; an API request should support that test rather than replace the behavior you need to exercise in a browser. See the API testing guide.
Capture a page after your Playwright run
Playwright can save screenshots directly, which is useful for local debugging and test artifacts:
page.screenshot(path="artifacts/home.png", full_page=True)
For a single element:
page.get_by_role("main").screenshot(path="artifacts/main.png")
Use a deterministic viewport, wait for the key assertion, and disable animations in your test environment when visual diffs must be stable. Remember that screenshots generated inside the test inherit the browser state and setup of that test.
Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF. Cookie and consent banners are accepted and 60+ known consent platforms, newsletter popups, and chat widgets are removed before capture; each step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result.
Using the API requires no local browser binary:
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
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)
Node.js
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 fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
See the ScreenshotNeo API documentation for all 63 options: full-page and CSS-selector capture, dark mode, device presets, retina scale, PDF paper and page settings, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed image links, async jobs with signed webhooks, bulk capture of up to 100 URLs per call, usage reporting, and the OpenAPI specification. An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.
Plans include 1,000 screenshots per month free with no card. Starter is $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; yearly billing gives two months free, and every feature is on every plan. Create a free ScreenshotNeo account and start with the included 1,000 screenshots.
Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Browser executable missing | Package installed without matching binaries, or Playwright was upgraded. | Run playwright install in the same environment and rebuild cached CI images. |
| Works headed, fails headless | Timing, viewport, or an environment-dependent browser behavior. | Use locators and assertions, capture a trace, and compare viewport and context settings. |
| Timeout waiting for a locator | Wrong role/name, hidden element, navigation issue, or a genuine application failure. | Inspect the trace, confirm the locator in Inspector, and assert the state that should precede the action. |
| Strict mode violation | A locator matches multiple elements. | Refine by role, name, filter, region, or test ID; avoid blind first/last selection. |
| Tests influence one another | Shared context, global cookies, or mutable server data. | Use the pytest fixtures, isolate accounts and data, and create explicit contexts for independent sessions. |
| Firefox/WebKit failure only | Engine-specific rendering, unsupported feature, or missing browser install. | Reproduce with the single engine, inspect the trace, and verify current browser configuration guidance. |
| Async hangs or behaves unpredictably | Mixed sync/async APIs or cancelled Playwright task. | Use one API style per module, await every operation, and avoid cancelling an in-flight Playwright call. |
Performance, reliability, and cost practices
- Reuse a browser process: launch once per worker and create isolated contexts for tests. Browser startup is more expensive than a new context.
- Control parallelism: match workers to CPU, memory, and the capacity of your test environment. Excessive concurrency can create resource contention and server throttling.
- Keep tests deterministic: fix locale, timezone, viewport, test data, and network dependencies where practical.
- Use retries carefully: a retry can expose intermittent infrastructure faults, but it should not hide a deterministic selector or product bug. Preserve traces for failed attempts.
- Cache browser layers in CI: invalidate the cache when the Playwright package or browser revision changes.
- Budget external work: API setup and third-party calls add latency. Prefer direct setup through
APIRequestContextwhen the UI does not need to create the state. - Screenshot cost: local Playwright screenshots consume your own runner resources. ScreenshotNeo bills only clean captures; bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and its headers show the verdict and billing state.
FAQ
Is Playwright better than Selenium for Python?
This guide does not establish a benchmark. Choose based on your team’s browser coverage, APIs, existing tests, and maintenance needs. Playwright offers Chromium, Firefox, and WebKit through one Python library and includes retrying locators and assertions.
Can I run Playwright without pytest?
Yes. Install playwright and use the standalone sync or async library. The pytest plugin is the recommended starting point for end-to-end suites.
Do I need to install browsers on every developer machine?
Any environment that launches Playwright needs matching browser binaries. Install them during local setup and in CI; rerun the install command after upgrades when required.
Should I use screenshots for assertions?
Use semantic locators and assertions for behavior. Use screenshots for visual review, debugging, or an intentional visual regression workflow with controlled rendering conditions.
Can AI agents capture screenshots?
Yes. ScreenshotNeo includes an MCP server with take_screenshot, get_page_info, and capture_pdf tools for MCP clients such as Claude and Cursor.


