ScreenshotNeo

BlogGuides

Python Guide to Selenium Element Locators

Learn every Selenium Python locator, when to use ID, CSS or XPath, how to wait and debug reliably, and how to avoid brittle selectors.

By the ScreenshotNeo team1 October 20269 min read

Selenium locators tell WebDriver which element to read or interact with. In Python, import By and pass a locator strategy plus a value to find_element:

from selenium.webdriver.common.by import By

username = driver.find_element(By.ID, "username")

Use find_element for one element and find_elements for a collection. Prefer a unique, stable ID. If no suitable ID exists, use a short CSS selector; use XPath when relationships or text predicates are genuinely useful.

1. Set up Selenium in Python

python -m pip install selenium

A complete example using Chrome is:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

options = webdriver.ChromeOptions()
options.add_argument("--headless=new")

driver = webdriver.Chrome(options=options)
wait = WebDriverWait(driver, 15)

try:
    driver.get("https://example.com")
    heading = wait.until(EC.visibility_of_element_located((By.TAG_NAME, "h1")))
    print(heading.text)
finally:
    driver.quit()

Recent Selenium releases can manage the browser driver for you. In a controlled build environment, install and pin the browser and driver versions according to your team’s setup.

2. The eight Selenium locator strategies

Strategy Python example Best use Main limitation
ID By.ID, "login" One element with a stable unique id Breaks when IDs are generated or changed
Name By.NAME, "email" Form controls with a stable name The value may not be unique
CSS selector By.CSS_SELECTOR, "form#login input[name='email']" Compact combinations of IDs, classes and attributes Becomes fragile when based on generated classes
XPath By.XPATH, "//button[@type='submit']" Relationships, text predicates and complex structures Complex or absolute expressions are harder to debug
Class name By.CLASS_NAME, "information" A single class token Compound class values are not accepted
Link text By.LINK_TEXT, "Selenium Official Page" An anchor with known visible text Applies only to links and changes with copy
Partial link text By.PARTIAL_LINK_TEXT, "Official Page" A stable substring of anchor text Can match the wrong repeated link
Tag name By.TAG_NAME, "button" Collecting a group such as all buttons Usually matches many elements

2.1 ID

element = driver.find_element(By.ID, "username")

IDs are Selenium’s preferred choice when they are unique, consistently predictable and owned by the application. Do not choose an ID that changes on every build or session.

2.2 Name

email = driver.find_element(By.NAME, "email")
email.send_keys("dev@example.com")

Name is useful for forms. Check that only the intended control has that name before relying on it.

2.3 CSS selectors

email = driver.find_element(
    By.CSS_SELECTOR,
    "form#login input[name='email']"
)
submit = driver.find_element(By.CSS_SELECTOR, "button[type='submit']")

CSS handles IDs, classes, attributes, descendants and direct children:

driver.find_element(By.CSS_SELECTOR, "#account .save-button")
driver.find_element(By.CSS_SELECTOR, "input[data-testid='search']")
driver.find_element(By.CSS_SELECTOR, "ul.results > li:first-child")

Keep selectors readable and anchored to attributes intended to remain stable, such as an ID, name, accessible label or deliberate test hook.

2.4 XPath

submit = driver.find_element(By.XPATH, "//button[@type='submit']")
row = driver.find_element(
    By.XPATH,
    "//tr[td[normalize-space()='Ada Lovelace']]"
)

Relative XPath is useful for text and relationships:

password = driver.find_element(
    By.XPATH,
    "//label[normalize-space()='Password']/following::input[1]"
)

Avoid absolute paths rooted at /html. A small wrapper or layout change can invalidate them. Selenium’s guidance describes XPath as flexible but generally harder to debug and potentially slower than a well-written CSS selector.

2.5 Class name

panel = driver.find_element(By.CLASS_NAME, "information")

Pass exactly one class token. For class="card information highlighted", use CSS instead:

panel = driver.find_element(By.CSS_SELECTOR, ".card.information.highlighted")
docs = driver.find_element(By.LINK_TEXT, "Selenium Official Page")
docs.click()

support = driver.find_element(By.PARTIAL_LINK_TEXT, "Official Page")

These strategies target anchor elements. If several links contain the same words, scope the selector to a stable container or use a more specific CSS or XPath locator.

2.7 Tag name

first_button = driver.find_element(By.TAG_NAME, "button")
all_buttons = driver.find_elements(By.TAG_NAME, "button")
for button in all_buttons:
    print(button.text)

Tag name is normally a collection strategy. Filter the returned elements deliberately rather than assuming the first match is the right one.

3. Choosing a robust locator

  1. Inspect the rendered DOM, not only the original HTML source.
  2. Look for a unique ID, stable name, accessible label or deliberate test attribute.
  3. Verify uniqueness in browser developer tools.
  4. Keep the locator short, readable and scoped to a stable component.
  5. Avoid generated class names, positional indexes and absolute DOM paths.
  6. Use find_elements when multiple matches are expected, then assert the count or filter the collection.

A practical preference order is:

  1. Stable unique ID
  2. Compact CSS selector using stable attributes
  3. Stable name or test hook
  4. Relative XPath for relationships or text
  5. Class, link text or tag name when their limitations are acceptable

Scope repeated components

card = driver.find_element(By.CSS_SELECTOR, "article[data-testid='plan-card']")
price = card.find_element(By.CSS_SELECTOR, ".price")
price.click()

Searching inside a stable parent prevents a page-wide selector from matching the same child in another component.

4. Waiting for elements correctly

A correct locator can still fail if the element has not been added, displayed or enabled. Prefer explicit waits over fixed sleeps.

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 15)
field = wait.until(EC.presence_of_element_located((By.ID, "email")))
field = wait.until(EC.visibility_of_element_located((By.ID, "email")))
button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "button[type='submit']")))
button.click()

presence_of_element_located means the node exists. visibility_of_element_located also requires it to be visible. element_to_be_clickable checks visibility and enabled state.

Wait for a collection

rows = wait.until(
    EC.presence_of_all_elements_located((By.CSS_SELECTOR, "table tbody tr"))
)
assert len(rows) > 0

Wait for a state or custom condition

wait.until(lambda d: d.find_element(By.ID, "status").text == "Ready")

Do not mix implicit and explicit waits casually; combined polling behavior can make failures slower and less predictable. Keep timeout values appropriate for the application and environment.

5. Selenium 4 relative locators

Relative locators help when a target is naturally described as above, below, beside or near another reliably located element.

from selenium.webdriver.common.by import By
from selenium.webdriver.support.relative_locator import locate_with

label = driver.find_element(By.ID, "email-label")
email = driver.find_element(
    locate_with(By.TAG_NAME, "input").below(label)
)

Use them when the geometric relationship is stable and clearer than a long XPath. They do not replace a unique ID or compact CSS selector when one is available.

6. Handling dynamic pages, frames and shadow DOM

Dynamic content

Locate after the page state changes and wait for the relevant condition. If a framework replaces a node, reacquire it instead of reusing an old element reference.

Nested iframes

frame = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "iframe.payment")))
driver.switch_to.frame(frame)
card_number = wait.until(EC.visibility_of_element_located((By.NAME, "cardnumber")))
driver.switch_to.default_content()

Shadow DOM

host = driver.find_element(By.CSS_SELECTOR, "user-profile")
shadow_root = host.shadow_root
avatar = shadow_root.find_element(By.CSS_SELECTOR, ".avatar")

A locator in the main document cannot directly see elements inside an iframe or shadow root. Switch context or query the shadow root first.

7. Complete form example

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
driver = webdriver.Chrome(options=options)
wait = WebDriverWait(driver, 20)

try:
    driver.get("https://example.com/login")
    wait.until(EC.visibility_of_element_located((By.ID, "login")))

    driver.find_element(By.NAME, "email").send_keys("dev@example.com")
    driver.find_element(By.CSS_SELECTOR, "input[type='password']").send_keys("secret")
    wait.until(
        EC.element_to_be_clickable((By.XPATH, "//button[@type='submit']"))
    ).click()

    message = wait.until(
        EC.visibility_of_element_located((By.CSS_SELECTOR, "[role='status']"))
    )
    print(message.text)
finally:
    driver.quit()

8. Troubleshooting locator failures

Error or symptom Likely cause Fix
NoSuchElementException Wrong selector, wrong page, delayed rendering or iframe Inspect the rendered DOM, wait explicitly and switch into the correct frame
TimeoutException The condition never became true Check the selector and page state; wait for visibility or clickability rather than increasing the timeout blindly
StaleElementReferenceException The application replaced the node Locate the element again after the update
Several elements match Selector is too broad Scope it to a stable ancestor or use find_elements and assert/filter
Class locator rejected A compound class string was passed to By.CLASS_NAME Pass one token or use a CSS selector
Link text does not work Target is not an anchor, text differs, or text is repeated Use CSS/XPath and inspect the actual anchor text
Element is present but cannot be clicked It is hidden, covered or disabled Wait for clickability, dismiss overlays and verify the enabled state
XPath works locally but breaks later Absolute path or generated attributes Anchor a relative XPath to a stable ancestor or attribute

9. Performance, reliability and maintenance

  • Use the shortest selector that expresses the intent; avoid unnecessary descendant levels.
  • Prefer stable IDs and CSS for routine lookups. XPath flexibility is valuable, but complex expressions are harder to maintain.
  • Wait for states instead of sleeping for a fixed duration. This reduces wasted time on fast runs and failures on slow runs.
  • Keep locator definitions in page objects or constants so a DOM change has one maintenance point.
  • Log the URL, locator strategy and value when a lookup fails, while avoiding sensitive form data.
  • Test selectors against realistic responsive layouts and authenticated states.
  • Use deliberate test hooks when the application team controls the markup.

10. Or skip the browser setup

If your goal is a page image rather than browser interaction, ScreenshotNeo returns a PNG, JPEG, WebP or PDF from one request. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing status. The service also provides an MCP server for AI agents, including Claude and Cursor.

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,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const file = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', file);

You can still choose full-page or element captures, dark mode, device presets, custom viewport and retina scale, waits, custom CSS and JavaScript, headers, cookies, user agent, timezone, geolocation, blocking rules, caching, signed links, asynchronous webhooks, bulk capture and PDF settings. One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

11. FAQ

Should I use CSS or XPath?

Use a stable ID first. Otherwise choose a compact CSS selector. Choose XPath when text predicates or element relationships make the target clearer.

Can I use a CSS selector with find_elements?

Yes. The strategy is independent of whether you request one element or a collection.

Why does Selenium find an element but fail to interact with it?

Presence does not guarantee visibility, enabled state or unobstructed position. Wait for the appropriate condition and inspect overlays or frames.

Are Selenium locators case-sensitive?

IDs, names, class tokens and most selector values must match the page’s actual attributes. Inspect the rendered DOM and use the exact value.

How do I locate an element by its text?

For anchors, use link text or partial link text. For other elements, use a relative XPath such as //button[normalize-space()='Save'], provided the text is stable.