Back to Testing guides

Getting Started with Playwright Testing in Python

Ayooluwa Isaiah
Updated on April 11, 2025

Automated testing is an essential aspect of modern web application development. It helps ensure that your application works correctly across different browsers and platforms, and that new changes don't break existing functionality.

While there are several testing frameworks available, Playwright has emerged as a powerful option that offers robust capabilities for testing web applications.

In this guide, we'll explore how to use Playwright with Python to create effective, reliable end-to-end tests for web applications. We'll cover everything from basic setup to advanced features, providing practical examples along the way.

What is Playwright?

Playwright

Playwright is a modern browser automation framework that allows you to control Chromium, Firefox, and WebKit browsers with a single API. It was developed by Microsoft and has gained popularity due to its cross-browser support, reliability, and powerful features.

When used with Python, Playwright enables developers and QA engineers to write concise, reliable tests that can detect issues across different browsers. Its architecture is designed to be fast and dependable, with built-in auto-waiting mechanisms that reduce the flakiness that plagues many end-to-end testing solutions.

Some key benefits of Playwright for Python developers include:

  • Support for all major browser engines (Chromium, Firefox, WebKit)
  • Powerful auto-waiting mechanisms that improve test reliability
  • Headless and headed mode for debugging
  • Ability to test responsive designs and mobile viewports
  • Network interception capabilities
  • Strong isolation between test cases

Let's dive into setting up Playwright and creating our first test.

Setting up your environment

To get started with Playwright for Python, you'll need to set up your environment. This involves installing Python (if you haven't already), Playwright, and the browser engines you want to test with.

First, ensure you have a recent version of Python installed, then create and activate a virtual environment for your testing project:

 
python -m venv venv
 
source venv/bin/activate

Now, install the Playwright package using pip:

 
pip install playwright

After installing the package, you'll need to install the browser engines that Playwright will control:

 
playwright install

This command installs Chromium, Firefox, and WebKit browsers on your system. The output should look like this:

Output
Downloading browsers...
Chromium downloaded to: /home/user/.cache/ms-playwright/chromium-1045
Firefox downloaded to: /home/user/.cache/ms-playwright/firefox-1365
WebKit downloaded to: /home/user/.cache/ms-playwright/webkit-1622
Browsers downloaded successfully.

Playwright downloading browser

Creating a project structure

Let's create a basic project structure for our tests:

 
mkdir playwright-demo && cd playwright-demo
 
mkdir tests
 
touch tests/__init__.py
 
touch tests/test_example.py

This creates a simple structure with a tests directory where we'll put our test files.

Writing your first Playwright test

Now that you have your environment set up, let's create a simple test. We'll test a basic scenario: navigating to a website and verifying its title.

Open the tests/test_example.py file and add the following code:

tests/test_example.py
import re
from playwright.sync_api import Page, expect

def test_has_title(page: Page):
    # Navigate to the Playwright website
    page.goto("https://playwright.dev/")

    # Expect the page title to contain "Playwright"
    expect(page).to_have_title(re.compile("Playwright"))

This simple test:

  1. Imports the necessary modules from Playwright.
  2. Defines a test function that takes a page parameter (which will be automatically provided by the Playwright test runner).
  3. Navigates to the Playwright website.
  4. Verifies that the page title contains the word "Playwright".

Playwright homepage

Running your first test

To run this test, you'll need a test runner. Playwright works well with pytest, which is the recommended test runner for Python. Let's install it:

 
pip install pytest pytest-playwright

Now, run your test using pytest:

 
pytest tests/test_example.py -v

You should see output similar to:

Output
============================== test session starts ==============================
platform linux -- Python 3.9.7, pytest-7.3.1, pluggy-1.0.0
rootdir: /home/user/playwright-demo
plugins: playwright-0.3.0
collected 1 item

tests/test_example.py::test_has_title PASSED                             [100%]

=============================== 1 passed in 5.78s ===============================

Playright first test

Congratulations! You've just run your first Playwright test with Python.

In the next section, we'll dive deeper into the core concepts of testing with Playwright.

Core testing concepts

Playwright uses a three-level architecture:

  1. Browser: The browser instance (Chromium, Firefox, WebKit).
  2. Context: An isolated browser session (similar to an incognito window).
  3. Page: A single tab within a browser context.

When you run tests with pytest-playwright, the test runner automatically handles the lifecycle of these objects. However, sometimes you might want more control over them.

Here's an example of handling browser and context explicitly:

 
from playwright.sync_api import sync_playwright

def test_multiple_pages():
    with sync_playwright() as p:
        # Launch a browser
        browser = p.chromium.launch(headless=False)

        # Create a context
        context = browser.new_context()

        # Create multiple pages in the same context
        page1 = context.new_page()
        page1.goto("https://example.com")

        page2 = context.new_page()
        page2.goto("https://playwright.dev")

        # Verify content on both pages
        assert "Example Domain" in page1.title()
        assert "Playwright" in page2.title()

        # Clean up
        context.close()
        browser.close()

In this example, we explicitly create a browser, a context, and multiple pages. This approach gives you fine-grained control over the lifecycle of these objects.

Locating elements

Playwright provides several ways to locate elements on a page:

  • CSS selectors
  • Text content
  • XPath
  • Test IDs
  • Accessibility selectors

Here's an example demonstrating different locator methods:

 
from playwright.sync_api import Page, expect

def test_different_locators(page: Page):
    page.goto("https://demo.playwright.dev/todomvc")

    # CSS selector
    new_todo = page.locator(".new-todo")

    # Locate by placeholder text
    new_todo_alt = page.locator("input[placeholder='What needs to be done?']")

    # Fill the input
    new_todo.fill("Learn Playwright")
    new_todo.press("Enter")

    # Locate by text content
    todo_item = page.locator("text=Learn Playwright")
    expect(todo_item).to_be_visible()

    # Locate by test ID (requires data-testid attribute in the HTML)
    # todo_list = page.locator("data-testid=todo-list")

    # Locate using XPath
    completed_checkbox = page.locator("//li//input[@type='checkbox']")
    completed_checkbox.click()

    # Verify item is completed
    completed_item = page.locator(".completed")
    expect(completed_item).to_be_visible()

It's generally recommended to use locators that are resilient to UI changes. Test IDs are particularly good for this, as they don't change when the UI design is updated.

Interacting with elements

Playwright makes it easy to interact with page elements just like a real user would. Here are some common interactions:

 
from playwright.sync_api import Page, expect

def test_interactions(page: Page):
    page.goto("https://demo.playwright.dev/todomvc")

    # Type into an input field
    new_todo = page.locator(".new-todo")
    new_todo.fill("Learn Playwright")

    # Press a key
    new_todo.press("Enter")

    # Click an element
    page.locator(".toggle").click()

    # Double-click
    # page.locator(".some-element").dblclick()

    # Hover over an element
    page.locator(".clear-completed").hover()

    # Drag and drop
    # source = page.locator(".source-element")
    # target = page.locator(".target-element")
    # source.drag_to(target)

    # Select option from dropdown
    # dropdown = page.locator("select")
    # dropdown.select_option(label="Option Text")

    # Work with multiple elements
    all_items = page.locator("li.todo")
    assert all_items.count() == 1

    # Get element text
    item_text = page.locator("label").text_content()
    assert item_text == "Learn Playwright"

Playwright's interaction methods are designed to wait for elements to be actionable before performing the action, which helps create more reliable tests.

Assertions and expectations

Playwright provides a rich set of assertions to verify conditions in your tests. These are implemented through the expect API:

 
from playwright.sync_api import Page, expect

def test_assertions(page: Page):
    page.goto("https://demo.playwright.dev/todomvc")

    # Add a todo item
    new_todo = page.locator(".new-todo")
    new_todo.fill("Learn Playwright assertions")
    new_todo.press("Enter")

    todo_item = page.locator(".todo-list li")

# Visibility assertions
expect(todo_item).to_be_visible()
# Text assertions
expect(todo_item).to_have_text("Learn Playwright assertions")
# Attribute assertions
expect(todo_item).not_to_have_class("completed")
# Mark item as completed page.locator(".toggle").click()
# Now check it has the completed class
expect(todo_item).to_have_class("completed")
# Count assertions page.locator(".new-todo").fill("Another item") page.locator(".new-todo").press("Enter") all_items = page.locator(".todo-list li")
expect(all_items).to_have_count(2)
# State assertions completed_checkbox = page.locator(".toggle").first
expect(completed_checkbox).to_be_checked()

These assertions not only verify conditions but also automatically wait for those conditions to be met, which helps reduce test flakiness.

Testing real user scenarios

Now that you understand the basics, let's create a more realistic test that simulates a user interacting with a to-do application.

tests/test_todo_app.py
from playwright.sync_api import Page, expect


def test_todo_workflow(page: Page):
    # Navigate to the TodoMVC application
    page.goto("https://demo.playwright.dev/todomvc")

    # Create several todo items
    create_todo_items(page, ["Buy groceries", "Pay bills", "Call mom"])

    # Verify all items are in the list
    todo_items = page.locator(".todo-list li")
    expect(todo_items).to_have_count(3)
    expect(todo_items).to_have_text(["Buy groceries", "Pay bills", "Call mom"])

    # Complete the second item
    todo_items.nth(1).locator(".toggle").click()

    # Verify the item is marked as completed
    expect(todo_items.nth(1)).to_have_class("completed")

    # Filter to show only active items
    page.locator("text=Active").click()

    # Verify only active items are shown
    visible_items = page.locator(".todo-list li:visible")
    expect(visible_items).to_have_count(2)
    #
    # # Filter to show only completed items
    page.get_by_role("link", name="Completed").click()

    # Verify only completed items
    visible_items = page.locator(".todo-list li:visible")
    expect(visible_items).to_have_count(1)

    # Clear completed items
    page.get_by_role("button", name="Clear completed").click()

    # Verify the completed item was removed
    page.get_by_role("link", name="All").click()
    todo_items = page.locator(".todo-list li")
    expect(todo_items).to_have_count(2)


def create_todo_items(page: Page, items):
    """Helper function to create multiple todo items."""
    for item in items:
        page.locator(".new-todo").fill(item)
        page.locator(".new-todo").press("Enter")

This test simulates a complete user workflow:

  1. Creating multiple to-do items.
  2. Marking an item as completed.
  3. Filtering the list to show only active or completed items.
  4. Clearing completed items.
 
pytest tests/test_todo_app.py -v

Testing todo apps

Advanced testing features

Playwright provides several advanced features that make it a powerful tool for end-to-end testing. Let's explore some of these features.

Network interception and mocking

Playwright allows you to intercept and modify network requests, which is useful for testing how your application behaves with different API responses:

 
import json
from playwright.sync_api import Page, Route, Request, expect

def test_network_interception(page: Page):
    # Mock API response for todos
    def handle_route(route: Route, request: Request):
        # Return a mock response
        route.fulfill(
            status=200,
            content_type="application/json",
            body=json.dumps([
                {"id": "1", "title": "Mocked Todo 1", "completed": False},
                {"id": "2", "title": "Mocked Todo 2", "completed": True}
            ])
        )

    # Route any request to the API endpoint
    page.route("**/api/todos", handle_route)

    # Navigate to the page that makes the API request
    page.goto("https://some-todo-app.example/")

    # Verify that the mocked data appears in the UI
    todo_items = page.locator(".todo-list li")
    expect(todo_items).to_have_count(2)
    expect(todo_items.first).to_have_text("Mocked Todo 1")
    expect(todo_items.nth(1)).to_have_class("completed")

This capability is particularly useful for testing edge cases and error conditions that might be difficult to reproduce with real API calls.

Authentication handling

Testing authenticated parts of your application can be challenging. Playwright makes this easier by allowing you to save and reuse authentication state:

 
from playwright.sync_api import Page, expect

def test_authenticated_workflow(page: Page):
    # Navigate to login page
    page.goto("https://demo.site/login")

    # Log in
    page.locator("input[name='username']").fill("test_user")
    page.locator("input[name='password']").fill("password123")
    page.locator("button[type='submit']").click()

    # Wait for successful login (dashboard page loads)
    expect(page.locator("h1")).to_have_text("Dashboard")

    # Save authentication state for reuse
    storage_state = page.context.storage_state()
    with open("auth.json", "w") as f:
        f.write(storage_state)

    # In future tests, you can reuse this authentication state:
    # browser.new_context(storage_state="auth.json")

By saving and reusing the authentication state, you can skip the login process in subsequent tests, making them faster and more focused.

Working with iframes and popups

Web applications often include iframes and popups, which can be tricky to test. Playwright provides mechanisms to interact with these elements:

 
from playwright.sync_api import Page, expect

def test_iframe_interaction(page: Page):
    page.goto("https://page-with-iframe.example/")

    # Get the iframe
    frame = page.frame_locator(".iframe-class").first

    # Interact with elements inside the iframe
    frame.locator("button").click()
    expect(frame.locator(".result")).to_have_text("Success")

def test_popup_handling(page: Page):
    page.goto("https://page-with-popup.example/")

    # Set up a listener for the popup
    with page.expect_popup() as popup_info:
        # Trigger the popup
        page.locator("button#open-popup").click()

    # Get the popup page
    popup = popup_info.value

    # Interact with the popup
    popup.locator("button#confirm").click()

    # Verify the result on the main page
    expect(page.locator(".status")).to_have_text("Popup confirmed")

These examples demonstrate how Playwright allows you to interact with complex page structures, including nested frames and popup windows.

Debugging Playwright tests

When tests fail, debugging them can be challenging. Playwright provides several tools to help with this process.

The Playwright Inspector is a graphical interface that helps you debug your tests. You can activate it by running your tests with the --debug flag:

 
PWDEBUG=1 pytest tests/test_example.py

This opens the Inspector, which allows you to:

  • Step through your test one action at a time.
  • Inspect the page state at each step.
  • Pick selectors visually.
  • Adjust timeouts and other settings.

Screenshot of Playwright Inspector

For more complex debugging scenarios, Playwright can record traces of your tests that can be viewed later:

tests/conftest.py
import pytest
from playwright.sync_api import Page

@pytest.fixture(scope="function", autouse=True)
def setup_teardown(page: Page):
    # Start tracing before each test
    context = page.context
    context.tracing.start(screenshots=True, snapshots=True)

    yield

    # Stop tracing and save the results
    context.tracing.stop(path="trace.zip")

With this fixture, Playwright will record detailed traces of all your tests. You can view these traces using the Playwright Trace Viewer:

 
playwright show-trace trace.zip

The Trace Viewer shows:

  • Screenshots at each step
  • Network requests
  • Console logs
  • Source code
  • And more

Screenshot of Trace Viewer

This tool is invaluable when debugging test failures, especially in CI environments where you can't directly observe the test execution.

Final thoughts

In this guide, we've explored how to set up Playwright, write basic and advanced tests, debug issues, and follow best practices for maintainable test code. These skills will help you build a reliable test suite that catches issues before they reach your users.

Thanks for reading, and happy testing!

Author's avatar
Article by
Ayooluwa Isaiah
Ayo is a technical content manager at Better Stack. His passion is simplifying and communicating complex technical ideas effectively. His work was featured on several esteemed publications including LWN.net, Digital Ocean, and CSS-Tricks. When he's not writing or coding, he loves to travel, bike, and play tennis.
Got an article suggestion? Let us know
Next article
PHP
Testing guides
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github