How to find Selenium elements with XPath

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.

Steps to find Selenium elements with XPath:

  1. Create selenium-find-xpath.py with a local page and XPath locators.
    selenium-find-xpath.py
    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.

  2. Run the script to confirm the XPath finds the intended card.
    $ python3 selenium-find-xpath.py
    title: Selenium XPath locator demo
    plan: Starter plan
    button: Choose Starter
    tier: starter
    status: selected starter
  3. Store the page-level XPath as a locator tuple in the real test.
    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.

  4. Search inside the found element with a relative XPath.
    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.

  5. Verify the selected element before using it in a page-object method.
    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.

  6. Replace copied absolute XPath values before committing the test.
    # 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.

  7. Remove the demo script after moving the locator into the real test.
    $ rm selenium-find-xpath.py