Playwright with Python

15Playwright with Python

What you will master here

  • Install + project structure with pytest-playwright
  • Sync vs async Python API — when to pick which
  • Pytest fixtures: page, context, browser, browser_context_args
  • Parity with TypeScript: locators, assertions, network mocking
  • Parallel execution with pytest-xdist
  • API testing with the request fixture
  • When Python beats TS for SDETs and when it doesn't

15.1 Install

pip install pytest-playwright pytest
playwright install --with-deps      # download Chromium / Firefox / WebKit
pytest --browser chromium

15.2 Sync vs async API

Sync (default with pytest)
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")
    print(page.title())
    browser.close()

Simple, sequential. Best for pytest tests, scripts, scraping.

Async
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")
        print(await page.title())
        await browser.close()

asyncio.run(main())

Use when integrating with async frameworks (FastAPI tests, asyncio orchestration).

15.3 First pytest test

# test_login.py
from playwright.sync_api import Page, expect

def test_login_happy(page: Page):
    page.goto("https://app.example.com/login")
    page.get_by_label("Email").fill("user@test.com")
    page.get_by_label("Password").fill("p4ssw0rd")
    page.get_by_role("button", name="Sign in").click()
    expect(page.get_by_role("heading", name="Dashboard")).to_be_visible()

Run: pytest -v. Add --headed to see the browser, --browser firefox to switch, --slowmo 500 to slow actions.

15.4 Built-in pytest fixtures

FixtureScopeWhat it gives you
pagefunctionFresh Page per test
contextfunctionFresh BrowserContext per test
browsersessionShared Browser
browser_namesession"chromium" / "firefox" / "webkit"
browser_context_argssessionDict passed to new_context — override to customise
browser_type_launch_argssessionDict passed to launch()

15.5 Custom fixtures (conftest.py)

# conftest.py
import pytest
from playwright.sync_api import Page

@pytest.fixture
def login_page(page: Page):
    """Reusable page object."""
    class LoginPage:
        def __init__(self, page): self.page = page
        def goto(self): self.page.goto("/login")
        def sign_in(self, email, pw):
            self.page.get_by_label("Email").fill(email)
            self.page.get_by_label("Password").fill(pw)
            self.page.get_by_role("button", name="Sign in").click()
    return LoginPage(page)

@pytest.fixture(scope="session")
def authed_user():
    """Worker-scoped — created once per session."""
    return {"email": "qa@test.com", "token": "abc123"}

# Override default browser context with custom settings
@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
    return {**browser_context_args,
            "viewport": {"width": 1440, "height": 900},
            "locale": "en-IN",
            "ignore_https_errors": True}

15.6 Locators & assertions — same as TS

Same priority order (role > label > placeholder > text), strict mode, narrowing, web-first assertions. Snake_case method names.

page.get_by_role("button", name="Buy")
page.get_by_label("Email")
page.get_by_placeholder("Search")
page.get_by_text("Hello", exact=True)
page.get_by_test_id("cart-count")
page.locator("input[name='email']")     # CSS
page.locator("xpath=//button[text()='Save']")

# Narrowing
page.get_by_role("row").filter(has_text="Alice")
page.locator(".row").nth(2)

# Assertions
expect(page).to_have_title("Dashboard")
expect(page.locator(".error")).to_have_text("Wrong password")
expect(page.get_by_role("button")).to_be_enabled()
expect(page.locator(".row")).to_have_count(3)

15.7 Network mocking

def test_empty_orders(page: Page):
    page.route("**/api/orders", lambda route: route.fulfill(
        status=200, content_type="application/json", body="[]"
    ))
    page.goto("/orders")
    expect(page.get_by_text("No orders yet")).to_be_visible()

15.8 API testing in Python

from playwright.sync_api import Playwright

def test_create_user(playwright: Playwright):
    request = playwright.request.new_context(base_url="https://api.example.com")
    response = request.post("/users", data={"email": "a@b.com"})
    assert response.status == 201
    body = response.json()
    assert body["email"] == "a@b.com"
    request.dispose()

15.9 Multiple tabs / popups

def test_opens_in_new_tab(context, page):
    with context.expect_page() as new_page_info:
        page.get_by_role("link", name="Terms").click()
    new_page = new_page_info.value
    new_page.wait_for_load_state()
    expect(new_page).to_have_url(re.compile(r"/terms"))

Python's equivalent of TS Promise.all is the with context manager — registers the listener, runs the action, captures the event.

15.10 Parallel execution

pip install pytest-xdist
pytest -n auto             # parallel across all CPU cores
pytest -n 4                # 4 workers

Each worker gets its own Browser; tests within a worker share it; each test gets its own Context.

15.11 Side-by-side — TS vs Python

TypeScript
test('login', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('a@b.com');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await expect(page.getByText('Dashboard')).toBeVisible();
});
Python
def test_login(page: Page):
    page.goto("/login")
    page.get_by_label("Email").fill("a@b.com")
    page.get_by_role("button", name="Sign in").click()
    expect(page.get_by_text("Dashboard")).to_be_visible()

15.12 When to pick Python over TS

Pick Python when…Pick TS when…
Your data/backend team writes in Python and shares librariesYour frontend is in TS and tests can share types/models
Heavy data-driven testing using pandas / numpyYou want strict typing in the test code itself
Pytest is the org's standard runnerYou want the Playwright Test runner's UI mode, trace viewer, projects, fixtures
You're scripting ad-hoc QA tools / scrapersYou're shipping an enterprise SDET framework
NotePython's Playwright is a 1:1 port of capabilities, but the Playwright Test Runner (with projects, sharding, traces UI) is JS/TS only. Python uses pytest, which has its own ecosystem but lacks the integrated trace viewer experience.

Module 21 — Python Playwright Q&A bank

How do you install Playwright for Python?
pip install pytest-playwright then playwright install --with-deps to fetch browsers. Run tests with pytest.
Sync vs async API in Python?
Sync (sync_playwright) is sequential — easiest with pytest, scripts. Async (async_playwright) integrates with asyncio for projects that already use async (FastAPI tests, async orchestration). Methods identical, just await them.
What built-in fixtures does pytest-playwright provide?
page, context, browser, browser_name, browser_type, browser_context_args, browser_type_launch_args. Page is fresh per test; browser is session-scoped.
How do you customise the browser context globally?
Override browser_context_args fixture in conftest.py. Return a dict that merges your overrides (viewport, locale, geolocation, ignore_https_errors) with the parent fixture.
How do locators differ between TS and Python?
Same priority order and behaviour; only the method names are snake_case (get_by_role, to_be_visible) instead of camelCase. Options are kwargs instead of options objects (name="Sign in" vs { name: 'Sign in' }).
How do you handle the promise-first pattern in Python?
Use the with context manager: with context.expect_page() as info: page.click('a'). Inside the with, the listener is active; the action triggers the event; info.value after the block returns the new page.
How do you run tests in parallel in Python?
pip install pytest-xdist, then pytest -n auto uses all CPU cores. Each worker has its own Browser; each test gets its own Context for isolation.
Can Python tests use the Playwright Trace Viewer?
Yes — enable tracing via context.tracing.start(...) and context.tracing.stop(path='trace.zip'). Open with playwright show-trace trace.zip. The viewer itself is the same tool.
Is the Python API 1:1 with TypeScript?
Capabilities yes — same locators, network mocking, traces, contexts. The Playwright Test runner (projects, UI mode, sharding) is TS/JS only; Python users use pytest's ecosystem (pytest-xdist for parallel, pytest-rerunfailures for retries, pytest-html for reports).
How would you do API testing in Python Playwright?
Use the playwright.request namespace: request = playwright.request.new_context(base_url=...), then request.get/post/put/delete(...). Returns a Response with .status, .json(), .headers.
When does Python Playwright shine?
When your team's tooling is Python (data engineers, pytest infra, pandas-heavy assertions), or you're stitching browser testing into a larger Python workflow. For green-field SDET work shipping enterprise frameworks, TS gives a richer integrated runner experience.