ScreenshotNeo

BlogHow-to

How to Use Python Locators in Selenium 4

Learn Selenium 4’s Python locator strategies, when to use find_element or find_elements, and how to handle waits, ambiguous matches, and shadow roots.

By the ScreenshotNeo team4 October 202610 min read

In Selenium 4 for Python, import By and pass a locator strategy plus its value to find_element or find_elements. Use find_element when you need one match; it returns the first matching element. Use find_elements when you need all matches; it returns a list, including an empty list when there are none.

from selenium.webdriver.common.by import By

email = driver.find_element(By.ID, "email")
rows = driver.find_elements(By.CSS_SELECTOR, "table tbody tr")

This guide covers Selenium’s eight traditional locator strategies, relative locators, shadow roots, waiting for dynamic content, common errors, and practical selector choices. The examples use the Selenium Python API documented by the Selenium project locator guide and its Python By API reference.

1. Set up Selenium and get a driver

Install the Selenium Python package in the environment that runs your script:

python -m pip install selenium

Recent Selenium versions include Selenium Manager, which can manage a compatible browser driver when you create a driver instance, provided the browser is installed and available. The example below opens a page and looks up an element. Replace the example URL and selector with a page and element you are authorized to automate.

from selenium import webdriver
from selenium.webdriver.common.by import By

URL = "https://www.selenium.dev/selenium/web/web-form.html"

driver = webdriver.Chrome()
try:
    driver.get(URL)
    text_box = driver.find_element(By.NAME, "my-text")
    text_box.send_keys("Selenium")
finally:
    driver.quit()

If your environment manages browser drivers separately, configure the driver according to your browser and Selenium setup. Locator calls work against the current browsing context: the current window, frame, or shadow root depending on where the search begins.

2. Choose between finding one element and finding all

Call Result When to use it No match
find_element(by, value) One WebElement, specifically the first match You expect one target, such as a form field or submit button Raises NoSuchElementException
find_elements(by, value) A list of matching WebElement objects You want to iterate over repeated items or explicitly check how many matched Returns an empty list

For example, a page can contain several buttons with the same class. A single-element lookup does not prove that the selector is unique: it selects the first match. If uniqueness matters, inspect the markup and make the selector more specific, then optionally check the count with find_elements.

from selenium.webdriver.common.by import By

buttons = driver.find_elements(By.CSS_SELECTOR, "button.save")
if len(buttons) != 1:
    raise RuntimeError(f"Expected one save button, found {len(buttons)}")
save_button = buttons[0]

3. The eight traditional locator strategies

Selenium’s Python By constants make the lookup strategy explicit. Use the attribute or relationship that identifies the intended element in the actual page DOM.

Strategy Matches Example
By.ID An element with the supplied id attribute By.ID, "lname"
By.NAME An element with the supplied name attribute By.NAME, "newsletter"
By.CSS_SELECTOR Elements matching a CSS selector By.CSS_SELECTOR, "form input[type=email]"
By.XPATH Elements matching an XPath expression By.XPATH, "//input[@value='f']"
By.CLASS_NAME An element with the supplied single class name By.CLASS_NAME, "submit-button"
By.TAG_NAME Elements with the supplied tag name By.TAG_NAME, "input"
By.LINK_TEXT An anchor whose visible text exactly matches By.LINK_TEXT, "Selenium Official Page"
By.PARTIAL_LINK_TEXT An anchor whose visible text contains the supplied text By.PARTIAL_LINK_TEXT, "Official Page"

Attribute locators: ID and name

Use an ID when the page exposes a stable, identifying ID. Use a name when that attribute identifies the field or control you need. These direct lookups are easy to read and avoid expressing relationships the task does not need.

first_name = driver.find_element(By.ID, "fname")
newsletter = driver.find_element(By.NAME, "newsletter")

CSS selectors

CSS selectors are useful for combining element types, attributes, classes, and relationships. Scope a selector to a meaningful region when the same attribute appears elsewhere.

# An email input inside a form
email = driver.find_element(By.CSS_SELECTOR, "form input[type='email']")

# A button within a specific dialog
confirm = driver.find_element(
    By.CSS_SELECTOR, "[role='dialog'] button.confirm"
)

XPath

XPath can locate elements by attributes and express relationships in the document tree. Quote string values correctly, and keep the expression tied to observable page structure.

female_radio = driver.find_element(By.XPATH, "//input[@value='f']")

# Find a button by its accessible label attribute
submit = driver.find_element(
    By.XPATH, "//button[@aria-label='Submit order']"
)

By.CLASS_NAME accepts one class name, not a space-separated list of multiple classes. For an element that has both primary and wide classes, use a CSS selector such as .primary.wide. Tag-name searches can match many elements, so they are often best combined with scoping or followed by a deliberate selection. Link-text strategies apply to anchors and use visible text; exact text can be fragile if the page changes wording.

submit = driver.find_element(By.CLASS_NAME, "submit-button")
all_inputs = driver.find_elements(By.TAG_NAME, "input")
help_link = driver.find_element(By.LINK_TEXT, "Help center")
settings_link = driver.find_element(By.PARTIAL_LINK_TEXT, "Settings")

4. How to choose a locator that stays understandable

  1. Inspect the rendered page’s DOM and identify an attribute or relationship that points to the intended element.
  2. Prefer a direct, meaningful attribute when one exists, such as a suitable ID, name, or accessible label.
  3. If needed, use CSS or XPath to combine conditions or scope the search to a specific region.
  4. Consider whether the expression can match more than one element. If it must be unique, verify that assumption in the page or code.
  5. Use relative locators only when the spatial relationship is genuinely clearer than a direct selector.
  6. If the target belongs to a shadow root or frame, switch or scope to that context before searching.

CSS and XPath are both supported and can express more than a direct attribute lookup. The right choice depends on the page markup and the intent the selector should communicate. The Selenium documentation does not establish a universal speed or reliability ranking among locator strategies, so avoid choosing a selector based on an assumed across-the-board performance advantage.

5. Wait for elements on dynamic pages

A locator searches when it is called. If JavaScript has not yet inserted the target, an immediate lookup can fail even though the element appears later. An explicit wait repeatedly checks a condition until it succeeds or the timeout expires.

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

URL = "https://www.selenium.dev/selenium/web/dynamic.html"
driver = webdriver.Chrome()
try:
    driver.get(URL)
    target = WebDriverWait(driver, 10).until(
        EC.visibility_of_element_located((By.ID, "revealed"))
    )
    target.send_keys("ready")
finally:
    driver.quit()

Common expected conditions include presence in the DOM, visibility, clickability, and presence of all elements located by a strategy. Choose the condition that matches the next action: presence does not guarantee visibility, and visibility does not guarantee that an overlay will not intercept a click.

# Wait until at least one matching row is present in the DOM
rows = WebDriverWait(driver, 10).until(
    EC.presence_of_all_elements_located(
        (By.CSS_SELECTOR, "table tbody tr")
    )
)

Use a bounded timeout that fits the application and environment. Avoid adding arbitrary sleeps as the default synchronization mechanism: fixed delays can be too short on a slow run and waste time on a fast one.

6. Selenium 4 relative locators

Relative locators identify an element by where it appears in relation to a known element: above, below, to_left_of, to_right_of, or near. Selenium uses JavaScript getBoundingClientRect() to determine element sizes and positions. Use this when the spatial relationship is a useful way to describe the target, rather than as a default replacement for an identifying attribute.

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

email_locator = locate_with(By.TAG_NAME, "input").above({By.ID: "password"})
email = driver.find_element(email_locator)

The origin can be described with a locator or an already located element. Relative matching still depends on the rendered page layout, so a responsive layout or repositioned elements can change which item is spatially above or near another.

7. Find an element inside a shadow root

A normal search from the document context does not search inside a component’s shadow root. Locate the host element, obtain its shadow root, then search within that root. The shadow root provides the context for the inner lookup.

from selenium.webdriver.common.by import By

host = driver.find_element(By.CSS_SELECTOR, "settings-panel")
shadow_root = host.shadow_root
checkbox = shadow_root.find_element(
    By.CSS_SELECTOR, "input[type='checkbox']"
)
checkbox.click()

If the component is created asynchronously, wait for the host before accessing its shadow root. Then use an explicit wait or an appropriate condition for the inner element if it appears later. Shadow DOM boundaries and the current browsing context matter: searching from the wrong root will not find the target.

8. Scope searches and reuse a located element

You can call find_element on a parent element to narrow the search to its descendants. This helps when repeated controls exist in separate cards or rows.

card = driver.find_element(By.CSS_SELECTOR, "article.product-card")
price = card.find_element(By.CSS_SELECTOR, ".price")
add_to_cart = card.find_element(By.CSS_SELECTOR, "button.add-to-cart")

A scoped selector reduces ambiguity in code, but the parent lookup must itself identify the intended section. If multiple cards match, choose the right card first using a stable attribute or a clear relationship.

9. Troubleshooting locator errors

Symptom Likely cause Fix
NoSuchElementException No element matched at lookup time, or the search used the wrong window, frame, or root Check the selector against the current DOM; wait for dynamic content; switch to the correct frame or shadow-root context
InvalidSelectorException Malformed CSS or XPath, or an invalid value for a strategy Validate the expression and use the correct locator type; use one class name with By.CLASS_NAME
The wrong element is returned find_element found the first of several matches Narrow the selector or parent scope; inspect matches with find_elements and check the count
StaleElementReferenceException The page replaced or detached the element after it was located Wait for the update to finish and locate the element again; do not keep using a reference to a replaced node
Element found but action fails It may be hidden, disabled, covered, or outside the expected state Wait for visibility or clickability as appropriate; check overlays and application state before acting
Text locator does not match Visible link text differs from the expected exact wording or the target is not an anchor Inspect rendered text; use a suitable attribute-based CSS or XPath locator if that better describes the target
Multiple-class lookup is invalid A space-separated class string was passed to By.CLASS_NAME Use one class or a CSS selector such as .primary.wide

10. Performance, reliability, and cost notes

  • Keep selectors specific enough. A broad selector can match unintended elements and force extra filtering in your code.
  • Wait for the condition you need. Explicit waits improve reliability on asynchronous pages without imposing a fixed pause on every run.
  • Re-find replaced elements. A WebElement refers to a particular element instance; page updates can make that reference stale.
  • Do not assume a universal selector speed winner. The cited Selenium documentation describes supported behavior, not a benchmark ranking.
  • Account for browser work. Runtime and resource use depend on page loading, browser startup, scripts, waits, and the amount of DOM work. Keep browser sessions scoped to the task and close them in a finally block.
  • Cost depends on execution environment. Selenium itself is a library; budget for the machines, browser infrastructure, network use, and any hosted execution service your setup uses. The research sources do not provide prices or benchmarks for those environments.

11. Or skip the browser setup

If your goal is a screenshot rather than interacting with DOM elements, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF; see the ScreenshotNeo API documentation for options and response details.

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)

ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card.

12. FAQ

Can find_element return more than one element?

No. It returns one element: the first matching result. Use find_elements to receive the collection.

Does find_elements throw an exception when there are no matches?

No. It returns an empty list, which you can check with if not elements:.

Can I use a relative locator as the only locator?

Yes, a relative locator can be passed to find_element; it describes the target through its spatial relationship to another element. A direct identifying selector is usually easier to understand when one is available.

Why can’t a document-level search find a shadow DOM element?

The search must run in the shadow root context. Locate the host and search through its shadow_root.

References