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.
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']"
)
Class, tag, and link text
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
- Inspect the rendered page’s DOM and identify an attribute or relationship that points to the intended element.
- Prefer a direct, meaningful attribute when one exists, such as a suitable ID, name, or accessible label.
- If needed, use CSS or XPath to combine conditions or scope the search to a specific region.
- Consider whether the expression can match more than one element. If it must be unique, verify that assumption in the page or code.
- Use relative locators only when the spatial relationship is genuinely clearer than a direct selector.
- 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
finallyblock. - 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.


