Python Playwright CSS Selector, XPath, and Text Locators
python playwright css selector xpath and text locators: Learn how to locate elements in Python Playwright with CSS selectors, XPath, and text-based locators, and choos...
Locating elements is the first step in almost every Playwright script. In Python Playwright, you have three primary ways to target an element: CSS selectors, XPath expressions, and text-based locators. Each has different syntax, different matching rules, and different failure modes. This article covers python playwright css selector xpath and text locators with concrete examples and the criteria that should drive your choice.
How Playwright Locators Work
Playwright's locator() method is the foundation of element targeting. It accepts a selector string and returns a Locator object. The locator does not resolve to a DOM element at creation time; it resolves when you perform an action such as click(), fill(), or text_content(). Playwright automatically waits for the element to be attached, visible, and stable before acting on it.
from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() page.goto("https://example.com") heading = page.locator("h1") print(heading.text_content()) browser.close()
The selector string passed to locator() can be a CSS selector, an XPath expression, or a text selector. Playwright infers the type from the syntax, or you can force a type with a prefix such as xpath= or text=.
CSS Selector Locators
CSS selectors are the default. When you pass a string without a prefix, Playwright treats it as CSS. This covers tag names, classes, IDs, attributes, and structural selectors.
page.locator("button.submit").click() page.locator("#login-form input[name='email']").fill("user@example.com") page.locator("div.card > h2").text_content()
Playwright also supports CSS pseudo-classes that go beyond standard CSS, such as :has(), :visible, and :text(). The :text() pseudo-class matches an element by its text content:
page.locator("button:has-text('Save')").click() page.locator("div:has(h2)").count()
CSS is the best default when the element has a stable ID, class, or attribute structure. It is concise, fast, and familiar to anyone who has written front-end code. The main limitation is text matching: standard CSS cannot select by text, so you must rely on Playwright's :text() extension or switch to another locator type.
XPath Locators
XPath is the second option. You can pass an XPath expression directly to locator(), but Playwright only detects it as XPath when the expression starts with // or ... To be explicit, use the xpath= prefix.
page.locator("xpath=//button[@type='submit']").click() page.locator("//div[@class='product' and contains(@data-id, 'sku')]").text_content()
XPath is more expressive than CSS when you need to traverse the DOM by relationship, such as finding a parent, a following sibling, or an element based on text in a different part of the tree.
page.locator("xpath=//label[contains(text(), 'Email')]/following-sibling::input").fill("user@example.com")
The tradeoff is readability. XPath expressions are harder to read and maintain than equivalent CSS, and they are slower to evaluate because the engine walks the DOM node by node rather than using the optimized CSS matching path.
Text-Based Locators
Text-based locators target elements by their visible text or accessibility attributes. These are separate methods on page and locator, not selector strings.
page.get_by_text("Submit").click() page.get_by_label("Email address").fill("user@example.com") page.get_by_placeholder("Enter your email").fill("user@example.com") page.get_by_role("button", name="Save changes").click()
get_by_text() matches by substring by default. Use exact=True for an exact match:
page.get_by_text("Save", exact=True).click()
get_by_role() is the most robust because it matches by ARIA role and accessible name, which is how assistive technology reads the page. When a page is built with proper semantics, role-based locators survive markup changes that would break CSS or XPath selectors.
The text= selector is the older string-based form. It still works inside locator():
page.locator("text=Submit").click()
Choosing Between CSS, XPath, and Text Locators
The choice depends on what the page gives you and what you are asserting.
| Criterion | CSS | XPath | Text/Role |
|---|---|---|---|
| Readability | High | Low | High |
| Matching by text | Limited (:text()) | Yes (contains()) | Native |
| DOM traversal | Limited | Full | Limited |
| Evaluation speed | Fastest | Slower | Fast |
| Resilience to markup changes | Moderate | Low | High |
Use CSS when the element has a stable class or attribute and you do not need text matching. Use XPath when you must traverse the DOM by relationship or match text inside a complex tree. Use text or role locators when the element's accessible name is stable and its markup is not.
A practical rule: prefer get_by_role() and get_by_label() for user-facing interactions, CSS for structural elements, and XPath only when neither of the first two can express the relationship.
Performance and Maintainability Considerations
XPath evaluation is consistently slower than CSS matching because XPath requires walking the DOM node by node. In a page with thousands of elements, a complex XPath expression can add measurable latency to every action. This rarely matters for a single action, but it compounds in loops and in test suites that run hundreds of locators.
Maintainability matters more than raw speed. XPath expressions that depend on the exact position of an element in the DOM break when the page structure changes. Text locators break when copy changes. CSS selectors break when class names change. Role-based locators break only when the accessible name or role changes, which is usually a deliberate product change.
The most fragile pattern is chaining XPath from the document root. A selector like //html/body/div[3]/div[1]/button fails the moment any wrapper is added. A relative locator scoped to a container, or a role-based locator, survives that change.
Common Pitfalls with Locators
Strict mode is the most common failure. By default, Playwright throws an error when a locator resolves to more than one element and you perform an action on it. This is intentional: acting on an ambiguous match hides bugs.
page.locator("button").click() # Strict mode violation if multiple buttons exist
If a locator legitimately matches multiple elements, narrow it with first, nth(), or filter():
page.locator("button").first.click() page.locator("li").nth(2).click() page.locator("button").filter(has_text="Delete").click()
Another common issue is mixing selector types without the prefix. Passing //button to locator() works because Playwright detects the leading //, but passing button[@type='submit'] without the xpath= prefix is treated as CSS and fails to match. Always use the prefix when the expression does not start with //.
Dynamic pages are another source of failures. Playwright auto-waits for the element to be attached and visible, but if the element is re-rendered after the locator resolves, the action can still fail. Locators are lazy, so re-querying page.locator(...) before each action avoids stale references instead of caching a resolved element.