mrkeyoor.com_
Wed 05 Aug 05:04 UTC
PyPITestingupdated 05 Aug 2026

playwright

Microsoft's Python bindings for Playwright: browser automation that drives Chromium, Firefox, and WebKit with a single API. It ships both a sync and an async flavor, and its locators auto-wait for elements to be actionable, which removes most of the sleep() calls that plague Selenium suites. Browser builds are pinned to the library version and installed by its own CLI. Teams use it for end-to-end testing (usually via the separate pytest-playwright plugin) and for scraping JavaScript-heavy sites.

Verdict

The current default for browser automation and E2E testing in Python: auto-waiting alone eliminates a whole class of flaky tests. The price is heavyweight browser downloads and a hard dependency on Microsoft's release train.

API stability5/5Locator-based API has been stable across the whole 1.x line; releases are frequent but additive, and deprecations (like old-style selectors) linger for years before removal.
Docs5/5playwright.dev/python mirrors the excellent Node docs with Python-specific snippets for both sync and async APIs, plus a full API reference and guides for auth, network, and CI.
Maintenance5/5Microsoft-maintained, pushed to within a day of this review, and browser builds (Chromium 151, Firefox 153, WebKit 26.5 per the README) track upstream releases closely.
Ecosystem4/5pytest-playwright, codegen, trace viewer, and the wider cross-language Playwright ecosystem; smaller Python-specific plugin scene than Selenium's decades of accumulated tooling.

Use it if

  • You need cross-browser automation (Chromium, Firefox, WebKit) from Python behind one API
  • You write E2E tests in pytest and want auto-waiting locators and retrying assertions instead of hand-rolled sleeps and flake
  • You scrape JS-heavy pages and need network interception, storage-state auth reuse, screenshots, or PDF export
  • You want parity with the Node, Java, and .NET Playwrights so knowledge transfers across teams
Skip it if

Setup reality

pip install playwright is only half the install: you then run `playwright install` to download browser builds, several hundred MB, and on fresh Linux CI you want `playwright install chromium --with-deps` so system libraries get apt-installed too. Sync and async APIs are separate imports that cannot be mixed in one context. pytest integration is a separate pytest-playwright package rather than part of this one. Browser builds are version-pinned, so upgrading the pip package without re-running the install command gives you an executable-not-found error, which is the single most common setup complaint.

Patterns

Install the package and browsersinstall-browsers

pip install playwright
playwright install chromium
# on fresh Linux CI:
playwright install chromium --with-deps

pip install alone is not enough; forgetting the browser install step causes the classic "Executable doesn't exist" error after every version bump.

Open a page and screenshot (sync API)sync-basic

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")
    page.screenshot(path="example.png")
    browser.close()

launch() is headless by default; pass headless=False to watch it. The sync API raises if called inside a running asyncio loop (e.g. Jupyter).

Async API for asyncio appsasync-basic

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())

Sync and async APIs are separate imports with identical shapes; pick one per process and do not mix them.

Find elements and interactlocate-and-interact

page.get_by_role("textbox", name="Email").fill("a@b.co")
page.get_by_label("Password").fill("hunter2")
page.get_by_role("button", name="Sign in").click()

Locators auto-wait until the element is visible and actionable, so explicit waits and sleeps are almost never needed; prefer role/label locators over CSS selectors.

Retrying assertions with expectassert-page-state

from playwright.sync_api import expect

expect(page.get_by_role("heading", name="Dashboard")).to_be_visible()
expect(page).to_have_url("https://app.example.com/home")
expect(page.get_by_test_id("count")).to_have_text("3")

expect() polls until the condition holds or times out (5s default); plain Python asserts on page state reintroduce the flake expect exists to kill.

E2E tests with pytestpytest-integration

# pip install pytest-playwright
from playwright.sync_api import Page, expect

def test_homepage(page: Page):
    page.goto("https://playwright.dev")
    expect(page).to_have_title("Playwright")  # substring? no: exact

# run: pytest --browser firefox --headed

The page fixture comes from the separate pytest-playwright package and gives each test a fresh isolated context; to_have_title matches exactly unless you pass a regex.

Log in once, reuse auth statereuse-login-state

# after logging in once:
context.storage_state(path="auth.json")

# in later runs/tests:
context = browser.new_context(storage_state="auth.json")
page = context.new_page()

storage_state captures cookies and localStorage; it goes stale when sessions expire, so regenerate it in CI rather than committing it.

Block or mock network requestsintercept-network

# block images to speed up scraping
page.route("**/*.{png,jpg,svg}", lambda route: route.abort())

# mock an API response
page.route("**/api/user", lambda route: route.fulfill(
    status=200, json={"name": "Test User"}))

Routes must be registered before goto/navigation triggers the requests; patterns are glob by default, regex also accepted.

Evaluate JavaScript in the pagerun-javascript

title = page.evaluate("() => document.title")
links = page.evaluate(
    "() => [...document.querySelectorAll('a')].map(a => a.href)")

Return values must be JSON-serializable; DOM nodes come back as handles, not usable objects, so map to primitives inside the page function.

Handle a file downloaddownload-file

with page.expect_download() as download_info:
    page.get_by_text("Export CSV").click()
download = download_info.value
download.save_as("/tmp/report.csv")

Wrap the click in expect_download before it happens; downloads are discarded when the context closes unless you save_as them.

Generate code by recording actionsrecord-codegen

playwright codegen https://example.com

Opens a browser plus inspector that writes Python for your clicks; great scaffolding, but the generated selectors usually deserve cleanup.

Debug with the Playwright inspectordebug-inspector

PWDEBUG=1 pytest test_login.py
# or pause anywhere in code:
page.pause()

PWDEBUG=1 forces headed mode and steps through actions; page.pause() drops you into the inspector mid-script.

Alternatives

PackageRegistryPick it when
seleniumPyPIYou need Grid infrastructure, vendor-provided drivers, or the broadest legacy browser coverage
seleniumbasePyPIBatteries-included Selenium framework, popular for scraping setups that need undetected modes
puppeteernpmYour team lives in Node and Chromium-only automation is enough