To automate or test a browser with Python, install Playwright and its browser binary, open a page, and interact through locators. Playwright automatically waits for many actionability conditions, while retrying expect assertions verify what a user can observe. Dedicated APIs handle downloads, screenshots, popups, and network responses. Combined with pytest isolation, this produces maintainable automation without coordinate clicks, brittle DOM paths, or arbitrary sleeps.

Playwright drives Chromium, Firefox, and WebKit through one API. It fits end-to-end tests, regression checks, and authorized internal workflows. Review Python testing with pytest for test fundamentals or Python task automation for work that does not require a browser.

Install Playwright and launch a browser

Use a Python virtual environment, then install the package, plugin, and one browser:

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

The second command downloads a browser revision compatible with the installed library. On Linux CI, python -m playwright install --with-deps chromium can install required operating-system libraries too. Check the official Playwright Python documentation for current platform instructions.

from playwright.sync_api import sync_playwright

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

The synchronous API is convenient for scripts and regular pytest tests. Choose async_playwright when the surrounding program already uses asyncio, and consistently await its methods. The guide to Python async and await explains that execution model. Mixing sync and async APIs creates avoidable complexity.

Build stable locators

A locator describes how to find an element and resolves it again for each operation. Prefer user-facing meaning or an explicit testing contract:

page.get_by_role("textbox", name="Email").fill("[email protected]")
page.get_by_label("Password").fill("test-only-secret")
page.get_by_role("button", name="Sign in").click()
page.get_by_test_id("profile-menu").click()

get_by_role aligns tests with accessible semantics. Labels, placeholders, and visible text also express intent. A stable data-testid is useful when text changes by locale or no suitable accessible role exists. CSS is available for special cases, but generated classes and selectors such as div:nth-child(4) couple tests to presentation.

Locators are strict when an operation requires one target. If two buttons match, the action fails instead of silently choosing one. Narrow the scope through a meaningful container, filter, or chained role locator. Avoid using .first merely to suppress ambiguity; make the expected target explicit.

Wait for outcomes, not elapsed time

Before clicking, Playwright checks whether the target is visible, stable, enabled, and able to receive events. This auto-waiting removes most reasons for time.sleep. Use retrying assertions for application outcomes:

from playwright.sync_api import expect

page.get_by_role("button", name="Save").click()
expect(page.get_by_role("status")).to_have_text("Saved")
expect(page).to_have_url("**/account")

Wait for the event that matters. Wrap an action in page.expect_response when a particular response is the requirement, or page.expect_popup when it opens a new tab. Do not make wait_for_load_state("networkidle") a universal fix: analytics and persistent connections may prevent idleness. An assertion about the final UI state describes the requirement more accurately.

Large global timeouts hide performance and synchronization defects while making failures slow. Keep a reasonable default and extend only operations with a known longer budget. Repeated flakes call for diagnosis, not additional seconds.

Verify downloads and capture screenshots

Begin listening before clicking so a fast download cannot be missed:

from pathlib import Path

with page.expect_download() as info:
    page.get_by_role("link", name="Download report").click()

download = info.value
destination = Path("artifacts") / download.suggested_filename
destination.parent.mkdir(exist_ok=True)
download.save_as(destination)
assert destination.stat().st_size > 0

Tests should save into pytest's temporary directory and inspect meaningful content, not just existence. Treat suggested filenames as untrusted input if automation writes outside an isolated test location.

Screenshots are diagnostic evidence rather than functional assertions:

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

Traces and images may expose passwords, tokens, customer details, or session cookies. Use synthetic accounts, redact sensitive fields where possible, and set short CI artifact retention. Never attach production data to a public issue.

Integrate Playwright with pytest

The pytest-playwright plugin supplies a fresh page fixture and handles cleanup:

from playwright.sync_api import Page, expect

def test_page_shows_heading(page: Page) -> None:
    page.goto("https://example.com")
    expect(page.get_by_role("heading", name="Example Domain")).to_be_visible()

Run it with pytest --browser chromium. A larger suite can configure a base URL, browser context options, screenshots, and tracing centrally. Keep every case independent. Reusing authenticated storage_state can speed setup, but that file contains credentials and belongs outside version control.

Test one observable behavior at a time. A single scenario that creates, edits, exports, and deletes a record is hard to diagnose and expensive to retry. Prepare data through an authorized API when appropriate, and use the UI for behavior that truly needs browser verification. The pytest guide covers fixtures, parametrization, and test organization.

Responsible and ethical automation

Technical access is not permission. Automate systems you own or have explicit authorization to test. Review terms of service, privacy obligations, and applicable law. Do not bypass CAPTCHA, authentication, paywalls, rate controls, or other access protections. Prefer a documented API when it meets the need.

Use least-privilege test accounts, separate environments, secret storage in CI, and bounded concurrency. Stop on repeated errors rather than increasing traffic against a struggling service. Collect only data required for the approved purpose and define deletion rules. For legitimate public-content extraction, the Python web scraping guide provides additional context.

Control browser state and make failures reproducible

Each browser context is an isolated session with its own cookies, local storage, permissions, locale, and viewport. Create a fresh context for independent scenarios instead of clearing state manually. When a workflow needs prior authentication, generate storage_state through an approved setup step, protect the resulting file as a credential, and give it a short lifetime. Do not share one mutable context across parallel tests because navigation, dialogs, and session changes can interfere with one another.

Reproduce environmental assumptions in configuration. Fix the test locale and timezone when formatting affects assertions, choose a representative viewport, and grant only permissions the scenario requires. Mocking every response can make a suite fast but unable to detect integration defects; reserve route interception for controlled dependencies and explicit error cases. Keep a smaller set of tests against a realistic environment.

Playwright tracing records actions, snapshots, network activity, and source locations. Enable traces for retries or failures rather than collecting every successful run indefinitely. The official Trace Viewer documentation explains how to inspect them. Treat trace archives as sensitive because they can contain page text, URLs, headers, and session details.

Common mistakes and checklist

  • Replace fixed sleeps with state assertions or event waits.
  • Select by role, label, stable text, or an explicit test ID.
  • Keep mutable browser state isolated between parallel tests.
  • Assert behavior instead of treating a screenshot as success.
  • Validate downloaded content in a temporary directory.
  • Keep browser and Python package installation in sync in CI.
  • Protect traces, cookies, screenshots, and authentication state.
  • Confirm authorization and traffic impact before running automation.
  • Keep scenarios short and failures attributable to one behavior.
  • Investigate flaky tests instead of masking them with retries.

A reliable suite starts with one critical user journey, semantic locators, outcome-based waits, and protected diagnostics. Run it locally and in CI, eliminate intermittent behavior, then expand coverage deliberately.