How to Use `expect` Assertions in Playwright for Python
Learn Playwright Python’s `expect` assertions for pages, locators, and API responses, with sync and async examples, retries, timeouts, and fixes.
expect is Playwright Python’s assertion API for stating what a page, locator, or API response should do. Web-specific assertions wait and retry until the condition passes or the assertion timeout expires, which makes them safer for asynchronously rendered interfaces than one-time value checks.
Import it from playwright.sync_api in synchronous tests or playwright.async_api in asynchronous tests. The default assertion timeout documented by Playwright is 5 seconds; configure a longer value globally or for a single assertion when the application needs it. See the Playwright Python Assertions guide and the LocatorAssertions API.
1. Install Playwright and create a test
python -m pip install pytest-playwright
playwright install
A synchronous pytest test can use the built-in page fixture:
from playwright.sync_api import expect
def test_checkout_page(page):
page.goto("https://example.com/checkout")
expect(page).to_have_title("Checkout")
expect(page.get_by_role("button", name="Submit")).to_be_enabled()
The assertion target determines what Playwright checks. Use a Page for URL and title assertions, a Locator for element state and content, and an APIResponse for HTTP response status.
2. Use locator assertions for elements
Locators are re-evaluated while an assertion waits. Prefer assertions such as to_have_text() and to_have_value() when content or form values can change after an action. The Locator documentation specifically recommends these waiting assertions to avoid flaky checks: Locator API.
from playwright.sync_api import expect
def test_profile_form(page):
page.goto("https://example.com/profile")
email = page.get_by_label("Email")
save = page.get_by_role("button", name="Save")
expect(email).to_be_visible()
expect(email).to_have_value("dev@example.com")
expect(save).to_be_enabled()
expect(page.get_by_role("status")).to_have_text("Profile ready")
Common locator matchers
| Matcher | Use it for |
|---|---|
to_be_visible() |
An element is visible to the user. |
to_be_hidden() |
An element is hidden or absent. |
to_be_enabled() / to_be_disabled() |
Whether a control can be used. |
to_be_checked() |
A checkbox or radio button state. |
to_have_text() |
Rendered text, including asynchronously updated text. |
to_have_value() |
The current value of an input or textarea. |
3. Assert page URL and title
from playwright.sync_api import expect
def test_redirect(page):
page.goto("https://example.com/login")
page.get_by_role("button", name="Sign in").click()
expect(page).to_have_url("https://example.com/dashboard")
expect(page).to_have_title("Dashboard")
Use a regular expression when part of the URL or title is variable:
import re
from playwright.sync_api import expect
expect(page).to_have_url(re.compile(r"/orders/\\d+$"))
expect(page).to_have_title(re.compile("Dashboard"))
The Python PageAssertions reference documents to_have_url() and to_have_title(): PageAssertions API.
4. Assert that an API response is successful
For an API request made through Playwright, expect(response).to_be_ok() passes for HTTP statuses from 200 through 299.
from playwright.sync_api import expect
def test_health_endpoint(playwright):
request = playwright.request.new_context()
response = request.get("https://example.com/api/health")
expect(response).to_be_ok()
request.dispose()
In asynchronous code, await the response assertion. See the APIResponseAssertions API.
5. Write asynchronous tests correctly
Import from playwright.async_api and await both asynchronous browser operations and assertions.
import pytest
from playwright.async_api import async_playwright, expect
@pytest.mark.asyncio
async def test_async_checkout():
async with async_playwright() as pw:
browser = await pw.chromium.launch()
page = await browser.new_page()
await page.goto("https://example.com/checkout")
await expect(page).to_have_title("Checkout")
await expect(
page.get_by_role("button", name="Submit")
).to_be_enabled()
await browser.close()
With the Playwright pytest plugin, use the async plugin and fixtures that match the versions installed in your project. Mixing sync objects with async assertions, or forgetting an await, produces misleading failures or coroutine warnings.
6. Configure assertion timeouts
Web assertions retry until they pass or their timeout is reached. The documented default is 5 seconds.
Set a global timeout
from playwright.sync_api import expect
expect.set_options(timeout=10_000)
Set a timeout for one assertion
from playwright.sync_api import expect
expect(page.get_by_role("status")).to_be_visible(timeout=15_000)
Choose a timeout that reflects the expected application behavior. A very short timeout makes legitimate rendering look flaky; an unnecessarily long timeout slows failure diagnosis.
7. Understand retry behavior
Playwright’s web-specific assertions repeatedly fetch the relevant page state and evaluate the matcher. For example, to_have_text() can wait for a status element that is populated after a network request. A plain Python comparison does not provide that behavior:
# Can be flaky if the UI has not rendered yet
assert page.get_by_role("status").inner_text() == "Saved"
# Preferred waiting assertion
expect(page.get_by_role("status")).to_have_text("Saved")
Do not describe every Python assertion as retrying. The retry and timeout behavior applies to Playwright’s documented web-specific assertions; ordinary Python expressions run immediately.
8. Soft assertions and version compatibility
The Next Playwright Python Assertions guide describes soft assertions as failures that do not stop the test immediately while still marking the test failed. It states that this requires pytest-playwright or pytest-playwright-asyncio 0.8.0 or newer. Because that guidance is served from the /next/ documentation path, verify the documentation and installed plugin version before depending on it.
# Check your installed versions before using soft assertions
python -m pip show pytest-playwright pytest-playwright-asyncio playwright
9. A complete sync example
from playwright.sync_api import Page, expect
def test_login_flow(page: Page):
page.goto("https://example.com/login")
expect(page).to_have_title("Sign in")
expect(page.get_by_label("Email")).to_be_visible()
page.get_by_label("Email").fill("dev@example.com")
page.get_by_label("Password").fill("correct-horse-battery-staple")
page.get_by_role("button", name="Sign in").click()
expect(page).to_have_url("https://example.com/dashboard")
expect(page.get_by_role("heading", name="Dashboard")).to_be_visible()
expect(page.get_by_role("status")).to_have_text("Signed in")
10. A complete async example
import pytest
from playwright.async_api import Page, expect
@pytest.mark.asyncio
async def test_async_login(page: Page):
await page.goto("https://example.com/login")
await expect(page).to_have_title("Sign in")
await page.get_by_label("Email").fill("dev@example.com")
await page.get_by_label("Password").fill("correct-horse-battery-staple")
await page.get_by_role("button", name="Sign in").click()
await expect(page).to_have_url("https://example.com/dashboard")
await expect(page.get_by_role("heading", name="Dashboard")).to_be_visible()
11. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
ImportError: expect |
Wrong API module or Playwright package is missing. | Install Playwright and import from playwright.sync_api or playwright.async_api. |
| Assertion times out | The locator does not match, the state never occurs, or the timeout is too short. | Check the locator, inspect the rendered state, and set a justified per-assertion timeout. |
| Text assertion is flaky | A one-time inner_text() comparison runs before rendering completes. |
Use expect(locator).to_have_text(). |
| Input value is wrong intermittently | The field is updated after an action or framework render. | Use to_have_value() and wait for the intended state. |
| Async test reports an unawaited coroutine | An async browser operation or assertion was not awaited. | Await every async Playwright call and assertion. |
| URL assertion fails after a click | The navigation is still in progress or the expected URL differs. | Use to_have_url(), verify the exact URL pattern, and avoid immediate string checks. |
| Soft assertion is unavailable | The installed pytest plugin may be older than the documented requirement. | Check installed versions and the matching Playwright documentation before enabling it. |
12. Performance, reliability, and cost notes
- Assertions that wait for a meaningful state reduce retries caused by race conditions, but excessive timeouts increase the time needed to report real failures.
- Use stable, user-facing locators such as roles and labels so assertions survive layout and implementation changes.
- Keep assertions close to the action that causes the state change; this makes timeout messages easier to interpret.
- Reuse a browser context where your test isolation model allows it, while keeping data and authentication isolated between tests.
- Playwright assertions themselves have no separate service charge. Your costs come from the machines, browsers, CI minutes, and any external services your tests call.
13. Or skip the browser setup
If your goal is a clean image of a page rather than an interactive browser test, ScreenshotNeo provides a single screenshot request. Its service accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. It also provides an MCP server for AI agents with take_screenshot, get_page_info, and capture_pdf.
See the ScreenshotNeo API documentation for all options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
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)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo includes full-page capture with lazy images loaded, CSS selector element capture, dark mode, device presets, custom viewports, retina scale, PDF output, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, cache TTLs, signed links, asynchronous jobs, webhooks, bulk capture, usage data, and an OpenAPI specification. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.
Create a free ScreenshotNeo account and get 1,000 screenshots each month with no card.
14. FAQ
Should I use a page assertion or a locator assertion?
Use a page assertion for URL or title. Use a locator assertion for an element’s visibility, state, text, or value.
Can I use expect outside pytest?
Yes. The sync and async assertion APIs can be used with your chosen test setup; pytest fixtures and plugin behavior are separate concerns.
Why does my assertion pass locally but fail in CI?
CI may render more slowly or expose a different page state. Verify the locator and condition, then choose a timeout that matches the real operation instead of adding arbitrary sleeps.
What does to_be_ok() accept?
It passes when the API response status is in the 200–299 range.


