XPath locators in Selenium help when a test needs an element described by nearby text, document structure, or an attribute relationship that a short ID or CSS selector cannot capture. A brittle XPath can make the test pass against the wrong node or fail after harmless markup changes, so the locator should target the smallest stable relationship and be verified against the page state it selects.
In Python Selenium, pass the expression through By.XPATH to find_element() or to a locator tuple used by WebDriverWait. The singular lookup returns the first matching element in the current search context, while a context lookup from an existing WebElement can keep a nested .// XPath inside one card, row, dialog, or form.
Prefer ID, name, or CSS selector locators when they identify the element plainly. Use XPath for relationships such as a button inside the card whose heading text matches a plan, and avoid absolute paths like /html/body/main/section[1]/button unless the test contract truly depends on that exact structure.
from pathlib import Path from tempfile import TemporaryDirectory import shutil from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait HTML = """<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>Selenium XPath locator demo</title> <script> window.addEventListener("DOMContentLoaded", () => { document.querySelectorAll("[data-action='choose-plan']").forEach((button) => { button.addEventListener("click", () => { const card = button.closest("[data-testid='plan-card']"); document.querySelector("#status").textContent = `selected ${card.dataset.tier}`; }); }); }); </script> </head> <body> <main> <section data-testid="plan-card" data-tier="starter"> <h2>Starter plan</h2> <p class="price">$19 per month</p> <button data-action="choose-plan">Choose Starter</button> </section> <section data-testid="plan-card" data-tier="enterprise"> <h2>Enterprise plan</h2> <p class="price">Contact sales</p> <button data-action="choose-plan">Choose Enterprise</button> </section> <p id="status">nothing selected</p> </main> </body> </html> """ CARD_XPATH = "//section[@data-testid='plan-card'][.//h2[normalize-space()='Starter plan']]" BUTTON_XPATH = ".//button[normalize-space()='Choose Starter']" options = Options() options.add_argument("--headless=new") options.add_argument("--window-size=1280,720") for browser_name in ("google-chrome", "chromium", "chromium-browser"): browser_path = shutil.which(browser_name) if browser_path: options.binary_location = browser_path break driver_path = shutil.which("chromedriver") service = Service(driver_path) if driver_path else Service() with TemporaryDirectory() as tmpdir: page = Path(tmpdir) / "xpath-locator.html" page.write_text(HTML, encoding="utf-8") driver = webdriver.Chrome(service=service, options=options) try: driver.get(page.as_uri()) wait = WebDriverWait(driver, 10) card = wait.until(EC.presence_of_element_located((By.XPATH, CARD_XPATH))) button = card.find_element(By.XPATH, BUTTON_XPATH) button.click() wait.until(EC.text_to_be_present_in_element((By.ID, "status"), "selected starter")) status = driver.find_element(By.ID, "status").text print(f"title: {driver.title}") print(f"plan: {card.find_element(By.TAG_NAME, 'h2').text}") print(f"button: {button.text}") print(f"tier: {card.get_dom_attribute('data-tier')}") print(f"status: {status}") finally: driver.quit()
The demo finds the card by heading text, searches inside that card for the matching button, clicks it, and checks the page text changed after the click.
$ python3 selenium-find-xpath.py title: Selenium XPath locator demo plan: Starter plan button: Choose Starter tier: starter status: selected starter
PLAN_CARD = ( By.XPATH, "//section[@data-testid='plan-card'][.//h2[normalize-space()='Starter plan']]", ) card = wait.until(EC.presence_of_element_located(PLAN_CARD))
normalize-space() prevents extra whitespace around the heading text from breaking the match.
button = card.find_element( By.XPATH, ".//button[normalize-space()='Choose Starter']", )
Keep the leading dot in .//button when the lookup must stay inside card. A bare //button expression can match from the document root instead of the element context.
assert card.get_dom_attribute("data-tier") == "starter" assert button.text == "Choose Starter"
The attribute and text checks confirm that the XPath selected the starter card rather than the first visually similar card on the page.
# Avoid a browser-copied path that depends on page position. "/html/body/main/section[1]/button" # Prefer an expression tied to stable page meaning. "//section[@data-testid='plan-card'][.//h2[normalize-space()='Starter plan']]//button"
Absolute paths and positional indexes usually break when layout wrappers, banners, or adjacent cards change.
$ rm selenium-find-xpath.py