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
| Fixture | Scope | What it gives you |
|---|---|---|
page | function | Fresh Page per test |
context | function | Fresh BrowserContext per test |
browser | session | Shared Browser |
browser_name | session | "chromium" / "firefox" / "webkit" |
browser_context_args | session | Dict passed to new_context — override to customise |
browser_type_launch_args | session | Dict 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 libraries | Your frontend is in TS and tests can share types/models |
| Heavy data-driven testing using pandas / numpy | You want strict typing in the test code itself |
| Pytest is the org's standard runner | You want the Playwright Test runner's UI mode, trace viewer, projects, fixtures |
| You're scripting ad-hoc QA tools / scrapers | You'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.