Skip to main content

Command Palette

Search for a command to run...

Playwright with Python and pytest: a complete, tested tutorial (from first script to CI)

Updated
•5 min read•View as Markdown
A
AutomationDataCamp (ADC) is an online software testing academy and QA consultancy. We teach test automation with Playwright, API testing, CI and AI-assisted testing, and prepare learners for ISTQB exams. Here we share practical tutorials from our courses. Free Playwright mini-course: automationdatacamp.com/en/

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_url

  • state: to_be_visible, to_be_enabled, to_be_checked

  • content: to_have_text, to_contain_text, to_have_value, to_have_count

  • negation: 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