Playwright with Python and pytest: a complete, tested tutorial (from first script to CI)
Playwright for Python installs with two commands and runs with pytest through the page fixture. This tutorial goes from a first script to GitHub Actions: locators, auto-retrying assertions, Codegen, Trace Viewer, API tests and a simple Page Object. The examples were run with Playwright 1.63 and pytest-playwright 0.9 before publication.
1. Install
Use a virtual environment:
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install pytest-playwright
playwright install
pytest-playwright installs both the playwright library and the pytest plugin. playwright install downloads Chromium, Firefox and WebKit (playwright install chromium for just one). On a fresh Linux machine or in CI, add --with-deps to install the system libraries the browsers need.
2. A first script (sync API)
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch() # headless by default
page = browser.new_page()
page.goto("https://playwright.dev/python/")
print(page.title())
page.screenshot(path="home.png")
browser.close()
Use p.chromium.launch(headless=False) to watch the browser. The async equivalent lives in playwright.async_api.
3. Your first pytest test
The plugin gives each test a fresh page in an isolated browser context, and closes everything afterwards.
# tests/test_home.py
import re
from playwright.sync_api import Page, expect
def test_page_title(page: Page):
page.goto("https://playwright.dev/python/")
expect(page).to_have_title(re.compile("Playwright"))
def test_get_started_link(page: Page):
page.goto("https://playwright.dev/python/")
page.get_by_role("link", name="Get started").click()
expect(page.get_by_role("heading", name="Installation")).to_be_visible()
Run pytest: two tests, Chromium, headless, 2 passed.
4. Pick locators users can see
| Locator | Targets | Example |
|---|---|---|
get_by_role |
Accessibility role + name | get_by_role("button", name="Send") |
get_by_label |
Form field by its label | get_by_label("Email address") |
get_by_text |
Visible text | get_by_text("Thank you") |
get_by_placeholder |
Input placeholder | get_by_placeholder("Search") |
get_by_test_id |
data-testid attribute |
get_by_test_id("submit") |
locator |
CSS/XPath, last resort | locator("#cart .total") |
If your app uses another attribute than data-testid, set it once: playwright.selectors.set_test_id_attribute("data-qa").
5. Web-first assertions
expect(...) retries until the condition is true or the timeout expires (5 s by default), unlike a plain assert that checks once:
page:
to_have_title,to_have_urlstate:
to_be_visible,to_be_enabled,to_be_checkedcontent:
to_have_text,to_contain_text,to_have_value,to_have_countnegation:
not_to_be_visible()
Keep assert for values you already have, such as an API JSON body.
6. Command-line options worth knowing
| Option | Effect |
|---|---|
--headed |
Show the browser |
--browser firefox |
chromium, firefox or webkit; repeat to run several |
--slowmo 500 |
Slow each action by 500 ms |
--base-url URL |
Lets you write page.goto("/path") |
--tracing on |
Record a trace per test (retain-on-failure keeps only failures) |
--screenshot only-on-failure |
Screenshot failing tests |
--video retain-on-failure |
Video of failing tests |
Put the ones you always use in pytest.ini:
[pytest]
pythonpath = .
addopts = --base-url https://playwright.dev --tracing retain-on-failure
7. Codegen and the Trace Viewer
playwright codegen --target python-pytest https://playwright.dev
Codegen records your clicks and writes a pytest function (-o test_generated.py saves it). Treat it as a draft: rename, add the assertions that matter, delete noise.
When a test fails, open its trace: DOM snapshots before and after each action, console, network and source.
pytest --tracing on
playwright show-trace test-results/<test-folder>/trace.zip
For live debugging, PWDEBUG=1 pytest -s opens the Playwright Inspector and pauses at each step.
8. API tests in the same tool
# tests/test_api.py
from typing import Generator
import pytest
from playwright.sync_api import Playwright, APIRequestContext, expect
@pytest.fixture(scope="session")
def api(playwright: Playwright) -> Generator[APIRequestContext, None, None]:
context = playwright.request.new_context(base_url="https://api.github.com")
yield context
context.dispose()
def test_playwright_python_repo(api: APIRequestContext):
response = api.get("/repos/microsoft/playwright-python")
expect(response).to_be_ok()
assert response.json()["name"] == "playwright-python"
Inside a UI test, page.request shares the browser cookies: handy to create data through the API before checking the screen.
9. A simple Page Object
# pages/home.py
from playwright.sync_api import Page
class HomePage:
def __init__(self, page: Page):
self.page = page
self.get_started = page.get_by_role("link", name="Get started")
def open(self):
self.page.goto("/python/")
def go_to_installation(self):
self.get_started.click()
# tests/conftest.py
import pytest
from playwright.sync_api import Page
from pages.home import HomePage
@pytest.fixture
def home(page: Page) -> HomePage:
return HomePage(page)
# tests/test_journey.py
from playwright.sync_api import expect
from pages.home import HomePage
def test_installation_journey(home: HomePage):
home.open()
home.go_to_installation()
expect(home.page.get_by_role("heading", name="Installation")).to_be_visible()
pythonpath = . in pytest.ini makes pages importable, and --base-url allows relative paths.
10. GitHub Actions
name: Playwright tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
tests:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements.txt
- run: python -m playwright install --with-deps
- run: pytest --tracing retain-on-failure
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-traces
path: test-results/
Pin pytest-playwright in requirements.txt so CI and laptops use the same browsers.
Python or TypeScript?
| Python (pytest-playwright) | TypeScript (@playwright/test) | |
|---|---|---|
| Runner | pytest and its plugins | Built-in runner |
| Parallel runs | pytest-xdist (pytest --numprocesses auto) |
Built-in workers |
| HTML report | pytest plugin | Built-in |
| UI mode, test agents | No | Yes |
If your team and app are already in Python, stay in Python. Starting from scratch or aiming at QA automation jobs, TypeScript gives the most complete tooling. Locators, web-first assertions, traces and Page Objects transfer one-to-one.
Adapted from our French tutorial Playwright Python : tutoriel complet. Written with AI assistance; the code was executed before publication.
AutomationDataCamp is an online software testing academy (Playwright, API testing, CI, AI-assisted testing, ISTQB prep). Playwright + TypeScript starter with CI: github.com/automationdatacamp/playwright-starter · automationdatacamp.com
Sources: Playwright for Python docs · microsoft/playwright-python
