Python Playwright Browser Page Locator: Click and Fill
python playwright browser page locator click and fill: Learn to use Playwright's Python locator API to click elements and fill form inputs, covering auto-waiting, stri...
When automating a browser with Python Playwright, the locator API is the core mechanism for finding elements and interacting with them. The python playwright browser page locator click and fill workflow covers two of the most common operations: clicking buttons and links, and entering text into form fields. This article explains how to use locators for both actions, how Playwright's auto-waiting behavior affects them, and where the API can fail.
Why Locators Replace Direct Page Calls
Older Playwright versions exposed methods directly on the page object:
page.click("#submit") page.fill("#email", "user@example.com")
These still work, but the locator API is the recommended approach:
page.locator("#submit").click() page.locator("#email").fill("user@example.com")
The difference matters. A locator is not a snapshot of an element; it is a description of where to find the element. Playwright re-resolves the locator each time an action is performed. If the element does not exist yet, or is re-rendered between actions, the locator still finds the current element. Direct page methods resolve the selector once and act on whatever is present at that moment.
Locators are also strict by default. If a selector matches more than one element, Playwright raises an error instead of silently acting on the first match. This prevents ambiguous actions from corrupting test state.
Creating a Locator
The page.locator() method accepts a CSS selector, a text selector, or a combination of both:
submit_button = page.locator("button.submit") email_field = page.locator("#email") search_input = page.locator("input[name='q']")
Text selectors are useful when an element has no stable CSS class:
page.locator("text=Save changes").click()
Playwright also supports chaining locators. The inner locator is resolved relative to the outer one:
form = page.locator("form.login") form.locator("input[type='submit']").click()
This keeps selectors scoped and reduces the chance of matching an element outside the intended container.
Clicking Elements
The click() method performs a full click sequence: it moves the mouse to the element, presses the button, and releases it.
page.locator("button.submit").click()
Playwright waits for the element to be actionable before clicking. Actionability means the element is visible, stable, enabled, and not covered by another element. If any condition is not met within the timeout, the call raises a TimeoutError.
The click() method accepts several options:
| Option | Effect |
|---|---|
button | Which mouse button to press: "left", "right", or "middle" |
click_count | Number of clicks, useful for double-click behavior |
delay | Milliseconds between mousedown and mouseup |
force | Skip actionability checks and click directly |
modifiers | Keyboard modifiers held during the click, such as ["Control"] |
position | Click at specific coordinates within the element |
A double-click, for example, uses click_count=2:
page.locator(".file-row").dblclick()
The dblclick() method is a convenience wrapper for click_count=2.
The force=True option bypasses actionability checks. Use it only when the element is intentionally hidden or covered, and you are certain the click will still reach the intended handler. Forcing a click on an element that is not visible can produce false positives in a test.
Filling Input Fields
The fill() method sets the value of an input, textarea, or contenteditable element:
page.locator("#email").fill("user@example.com")
fill() clears the existing value first, then inserts the new text. This is different from typing character by character. The browser's input event fires once with the complete value, which is faster and more deterministic than simulating keystrokes.
To clear a field without setting a value, pass an empty string:
page.locator("#search").fill("")
If a test must simulate real keyboard input, for example to trigger keypress handlers that depend on individual characters, use press_sequentially():
page.locator("#code").press_sequentially("ABC123", delay=50)
The delay parameter controls the pause between keystrokes. This is slower than fill() and should be reserved for cases where per-key events matter.
fill() does not move focus to the element before setting the value. If the page relies on focus events to enable a submit button, call focus() first:
field = page.locator("#email") field.focus() field.fill("user@example.com")
Auto-Waiting and Actionability
Playwright's auto-waiting is the main reason locator-based click and fill operations are reliable in dynamic pages. Before each action, Playwright checks that the element is:
- attached to the DOM
- visible
- stable (not animating or changing position)
- enabled
- not obscured by another element
If any check fails, Playwright retries until the condition is met or the timeout expires. The default timeout is 30 seconds, configurable per action or globally:
page.locator("#submit").click(timeout=5000)
For cases where an action is not needed but the element must exist, use wait_for():
page.locator(".toast-message").wait_for(state="visible")
For assertions, Playwright's expect() polls until the condition holds:
from playwright.sync_api import expect expect(page.locator(".success")).to_be_visible()
This is preferable to time.sleep() because it reacts as soon as the condition is true, reducing test runtime.
Strict Mode and Multiple Matches
A locator that matches multiple elements raises a strict mode violation when an action is attempted:
page.locator("button").click() # raises if more than one button exists
To target a specific match, narrow the selector or use positional methods:
page.locator("button").first.click() page.locator("button").nth(2).click() page.locator("button").last.click()
The filter() method narrows by text or attribute:
row = page.locator("tr").filter(has_text="Order #1234") row.locator("button.delete").click()
Strict mode is a safeguard. When it fires, the selector is too broad. Fix the selector rather than suppressing the error.
Common Failures and How to Resolve Them
The most frequent failure in click and fill operations is a TimeoutError. The element either never appears, or it appears but never becomes actionable. Common causes:
- The selector is wrong or too specific.
- The element is rendered by JavaScript after an async request that never completes.
- The element is covered by an overlay, such as a cookie banner or modal.
- The element is disabled until another field is filled.
A strict mode violation occurs when the selector matches multiple elements. The error message lists the matching elements, which helps identify whether the selector needs to be more specific or whether the page structure changed.
An element that is "not enabled" raises a TimeoutError even if it is visible. Playwright will not click a disabled button. The test should wait for the button to become enabled, which is usually a better assertion of application state than clicking and checking the result.
Keeping Locators Maintainable in Larger Suites
The most common source of flaky tests is brittle selectors. CSS classes change frequently during development, and deeply nested selectors break when the DOM structure is refactored.
Prefer stable attributes like data-testid for elements that tests interact with:
page.locator("[data-testid='submit-button']").click()
This decouples tests from presentation changes. If a team controls the application code, agreeing on a small set of test IDs reduces maintenance cost across the suite.
Chaining locators also improves maintainability. Scoping a locator to a container makes the intent clear and prevents accidental matches:
login_form = page.locator("form.login") login_form.locator("#username").fill("admin") login_form.locator("#password").fill("secret") login_form.locator("button[type='submit']").click()
When a test fails, the error message includes the locator chain, which makes the failure location obvious.