Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

The Page Object Model (POM) wraps a Playwright Page in a Python class that represents a screen or reusable application area. The class owns locators and exposes task-level methods such as search() or submit_order(). Tests then describe behavior instead of repeating selector and browser code. Playwright documents POM as an organizational pattern that is especially useful as a suite grows: selectors are collected in one place and operations become a higher-level API.

This guide shows how to build sync and async page objects, choose resilient locators, integrate them with pytest, diagnose common failures, and decide when the abstraction is worth adding.

How do I use the Page Object Model with Playwright and Python?

Create a class for a page or meaningful application area, pass a Playwright Page into its constructor, retain locators as attributes, and provide methods for user-level actions. A test creates the object and calls those methods.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  1. Install Playwright and its browser binaries, then configure your test runner.
  2. Identify the controls and outcomes that belong to one page or component.
  3. Define stable locators in __init__.
  4. Write focused methods that perform one meaningful operation.
  5. Keep assertions in the test unless a narrowly scoped page-state check is part of your team’s convention.

The object does not need a base class, inheritance hierarchy, or one class for every URL. Model the boundaries that make your tests easier to read and maintain.

How do I create a page object in Playwright Python?

Synchronous page object

This example follows Playwright’s documented structure. Replace the accessible name with the one exposed by your application.

from playwright.sync_api import Page

class SearchPage:
    def __init__(self, page: Page):
        self.page = page
        self.search_term_input = page.get_by_role("textbox", name="Search")
        self.results = page.get_by_role("main")

    def navigate(self) -> None:
        self.page.goto("https://example.test/search")

    def search(self, text: str) -> None:
        self.search_term_input.fill(text)
        self.search_term_input.press("Enter")

    def has_results(self) -> bool:
        return self.results.is_visible()

Methods should express an application task, not expose every low-level click. A method such as search() can later accommodate a changed submit button or loading sequence without changing every test.

Async page object

Use the async API consistently: declare methods with async def and await every Playwright operation.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from playwright.async_api import Page

class AsyncSearchPage:
    def __init__(self, page: Page):
        self.page = page
        self.search_term_input = page.get_by_role("textbox", name="Search")

    async def navigate(self) -> None:
        await self.page.goto("https://example.test/search")

    async def search(self, text: str) -> None:
        await self.search_term_input.fill(text)
        await self.search_term_input.press("Enter")

Do not mix sync calls into an async object or omit an await; those mistakes create confusing coroutine and timing errors.

Which locators should I use in a Playwright page object?

Start with user-facing attributes and explicit contracts. Playwright recommends prioritizing role locators because they match how users and assistive technologies perceive the page. The locator guide also documents labels, text, and test IDs.

Preferred choices

  • page.get_by_role("button", name="Save") for a uniquely named button.
  • page.get_by_label("Email") for a form control with an associated label.
  • page.get_by_text("Order complete") when visible text is the intended contract.
  • page.get_by_test_id("checkout-submit") when your team maintains explicit test IDs.

Test IDs are often stable when visible copy or roles change, but they are not user-facing. Treat them as a deliberate application-test contract rather than a default for every element.

Selectors to treat cautiously

CSS and XPath remain available through page.locator(), but long chains tied to DOM nesting or styling classes are fragile. A selector such as div:nth-child(2) > section > button can fail after an innocuous layout change. Prefer a semantic locator, a label, or a short attribute contract.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Actions are strict: if a locator matches multiple elements, Playwright raises an error rather than guessing. Refine the locator with a role, name, filter, or parent region. Using .first, .last, or .nth() should be an intentional choice, not a routine way to silence strictness; a changing page can make the position point at the wrong control.

Dynamic collections

Locators resolve against the current page when an action runs, which helps with re-rendered interfaces. The Locator API cautions that locator.all() does not wait for matches. Calling it while a list is still changing can produce an unpredictable or flaky result. Wait for a meaningful state first, or iterate using locator operations that retain Playwright’s waiting behavior.

How should page-object methods handle waiting?

Let Playwright’s locator actions perform their built-in actionability checks instead of inserting arbitrary sleeps. fill(), click(), and similar operations wait for the target to be ready. When a workflow has a real application state, wait for that state explicitly: a result heading, URL, response, or disappearance of a progress indicator.

def search_and_wait(self, text: str) -> None:
    self.search_term_input.fill(text)
    self.search_term_input.press("Enter")
    self.page.get_by_role("heading", name="Results").wait_for()

Keep waits close to the operation that requires them. A page object that sleeps for a fixed number of seconds hides slow behavior and still fails when the environment is slower than the chosen delay.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How do I use page objects with pytest?

The Playwright pytest plugin supplies a function-scoped page fixture and a context fixture, plus session-scoped Playwright and browser fixtures. A test can request page, construct the object, and assert the user-visible result.

from playwright.sync_api import Page, expect
from .pages.search_page import SearchPage

def test_search(page: Page):
    search = SearchPage(page)
    search.navigate()
    search.search("playwright")
    expect(page.get_by_role("heading", name="Results")).to_be_visible()

Install and configure the current plugin according to the Playwright pytest documentation. The runner supports Chromium, Firefox, and WebKit selection, headed mode, device emulation, screenshots, video, and traces. pytest-xdist can distribute tests across processes; choose a process count your hardware and test isolation can support, because excessive parallelism can cause unexpected behavior.

Async pytest tests

Async fixtures require the async integration documented by Playwright. The plugin documentation notes the pytest-playwright-asyncio setup and a pytest-asyncio version/configuration requirement. Check that page for the versions you are using, then keep the fixture, page object, and test consistently async.

import pytest
from playwright.async_api import Page, expect

@pytest.mark.asyncio
async def test_async_search(page: Page):
    search = AsyncSearchPage(page)
    await search.navigate()
    await search.search("playwright")
    await expect(page.get_by_role("heading", name="Results")).to_be_visible()

How should a page-object project be organized?

A small suite can keep one module per page. As the suite expands, separate page objects, reusable components, fixtures, and tests:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
tests/
  pages/
    search_page.py
    checkout_page.py
  components/
    cookie_banner.py
  test_search.py
  conftest.py

Page objects versus component objects

If the same navigation bar, date picker, or dialog appears on many pages, model that area as a component object and compose it into page objects. Playwright’s POM guidance allows an object to represent a part of an application; it does not require a one-class-per-URL architecture.

Keep abstractions at the right level

A useful method represents a business or user action: add_item(name), complete_payment(), or search(term). Avoid methods that merely rename every single locator call, and avoid turning the page object into a second test runner that conceals the behavior under test. Expose enough state for tests to make clear assertions.

Should I use sync or async Playwright in Python?

Choice Use it when What to enforce
Sync API Your tests and fixtures are ordinary synchronous Python. Import from playwright.sync_api; do not await calls.
Async API Your application or test stack already uses asyncio. Import from playwright.async_api; await navigation, actions, assertions, and fixture calls.

Neither style is inherently more correct. Choose the one that matches the surrounding runtime and avoid mixing them in the same object or fixture layer.

When should I use a page object instead of calling Playwright directly?

Direct calls are clear for a short test or a one-off experiment. Introduce a page object when selectors and workflows repeat, when several tests share navigation or setup, or when a page’s DOM changes often enough that centralizing its contract will help maintenance. The payoff is organizational rather than a guaranteed performance improvement; the official guide describes intended authoring and maintenance benefits, not a measured time-saving percentage.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Keep direct calls when an abstraction would hide the assertion or add indirection without reuse. Review page objects periodically: delete dead methods, split classes that represent unrelated areas, and keep locator names understandable.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Common failures and fixes

Strict mode violation

Cause: the locator matches multiple elements. Fix: add the correct role, accessible name, label, container filter, or test ID. Do not default to .first unless order is genuinely part of the requirement.

Timeout waiting for an element

Cause: the name or role is wrong, the page has not navigated, an overlay blocks the control, or the application is still loading. Fix: inspect the accessibility tree, verify the URL and locator, wait for a real state, and capture a trace or screenshot with the pytest options.

Flaky list assertions

Cause: locator.all() was called while the list was changing. Fix: wait for a stable heading/count or assert through locator methods that retain auto-waiting.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Async errors or unawaited coroutines

Cause: sync and async APIs were mixed, or an async method was called without await. Fix: use one API throughout the fixture, page object, and test, and mark async tests using the integration required by your installed plugin versions.

Tests interfere when run in parallel

Cause: shared accounts, files, ports, or server data. Fix: isolate test state, use independent contexts and data, and reduce the xdist worker count when the environment cannot safely support more processes.

Or skip the browser setup

If your goal is a clean website image rather than an end-to-end browser test, ScreenshotNeo returns a screenshot or PDF from one request. It accepts consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

See the complete parameter reference in the ScreenshotNeo documentation. The same endpoint supports full-page and element captures, device presets, custom viewports, dark mode, retina scale, PDF options, CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, asynchronous webhooks, bulk capture, usage data, and an OpenAPI specification.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Create a free ScreenshotNeo account to try it.

FAQ

Does Playwright require page objects?

No. POM is an optional organizational pattern; Playwright also supports direct page and locator calls.

Can one object represent only part of a page?

Yes. A reusable dialog, menu, or form can be modeled as a component object and composed into larger page objects.

Are page objects a replacement for assertions?

No. They organize interactions and selected state checks; tests still need explicit assertions for the behavior they verify.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently Asked Questions

Does Playwright require page objects?

No. POM is optional; direct page and locator calls are valid for small or one-off tests.

Can one object represent only part of a page?

Yes. Reusable dialogs, menus, and forms can be component objects composed into page objects.

Are page objects a replacement for assertions?

No. Tests still need explicit assertions for the behavior they verify.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.