Resource library

QA How-To

Selenium findElement and findElements in Python (2026)

Master selenium findElement and findElements python usage with current snake_case APIs, explicit waits, scoped searches, list checks, and page objects.

22 min read | 3,030 words

TL;DR

The Python equivalents of Selenium findElement and findElements are `driver.find_element(By..., value)` and `driver.find_elements(By..., value)`. The singular method returns the first match or raises `NoSuchElementException`; the plural method returns a Python list and gives you `[]` when nothing matches.

Key Takeaways

  • Python uses `find_element` and `find_elements`, not the Java-style camelCase method names.
  • Use `find_element` for a required single match and expect `NoSuchElementException` when it is absent.
  • Use `find_elements` for repeated or optional nodes, and handle its empty list result explicitly.
  • Pass locator tuples to `WebDriverWait` expected conditions so dynamic elements are found during polling.
  • Scope descendant lookups to forms, cards, rows, dialogs, frames, or shadow roots to remove ambiguity.
  • Convert WebElement collections into stable domain values before making order, count, or uniqueness assertions.
  • Do not let implicit waits, broad exception handling, or positional selectors conceal a broken application state.

Searches for selenium findElement and findElements python often mix Java terminology with Python syntax. In current Selenium Python, call find_element for one required element and find_elements for a collection or optional presence. The singular call raises NoSuchElementException when it cannot find a match, while the plural call returns an empty list.

The method name is only the start. Reliable tests also need an intentional locator, the correct browser context, and a wait tied to the UI state. This guide builds those pieces into runnable Python examples and framework patterns that remain readable when a failure reaches CI.

TL;DR

from selenium.webdriver.common.by import By

heading = driver.find_element(By.CSS_SELECTOR, "main h1")
rows = driver.find_elements(By.CSS_SELECTOR, "table tbody tr")

assert heading.text == "Orders"
assert len(rows) >= 1
Need Python call No-match result
One required node find_element(by, value) Raises NoSuchElementException
All current matches find_elements(by, value) Returns []
Optional presence bool(find_elements(...)) False
A dynamic visible node WebDriverWait(...).until(EC.visibility_of_element_located(locator)) Raises TimeoutException after the wait
A dynamic collection Wait for a list or count condition Raises TimeoutException if the condition never succeeds

Do not use find_elements(...)[0] as a casual substitute for find_element. Choose the API that states whether zero, one, or many matches are valid.

1. Selenium findElement and findElements Python Naming

The Python binding follows PEP 8 snake_case. The current methods are find_element and find_elements. Code copied from Java such as driver.findElement(By.id("email")) will not run in Python. Older Python convenience calls such as find_element_by_id were replaced by the unified By API.

from selenium.webdriver.common.by import By

email = driver.find_element(By.ID, "email")
products = driver.find_elements(By.CSS_SELECTOR, "[data-testid='product']")

The first argument names the strategy and the second carries its value. Common constants include By.ID, By.NAME, By.CSS_SELECTOR, By.XPATH, By.LINK_TEXT, By.PARTIAL_LINK_TEXT, By.CLASS_NAME, and By.TAG_NAME. Using By makes strategies easy to store as (by, value) tuples, which is also the shape expected by Selenium's standard expected conditions.

Both the driver and each WebElement provide these finder methods. Driver calls search the active document. Element calls search descendants of that element. An open ShadowRoot also supports finder methods. This shared capability is the basis for component-level page objects.

Python type hints help make cardinality visible. A wrapper for a required node should return WebElement; a collection query should return list[WebElement]; and an optional component can return WebElement | None only if the framework makes that optional contract explicit. Avoid returning unrelated types from the same helper based on what happened at runtime.

2. Run a Self-Contained Python Example

Install Selenium in an isolated environment with python -m pip install selenium. A recent Selenium 4 release includes Selenium Manager, which handles common driver discovery and setup when webdriver.Chrome() starts. You still need a compatible browser and normal network or execution permissions for first-time driver setup.

This script uses a URL-encoded in-memory document, so its assertions are not tied to an external demo site:

from urllib.parse import quote

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

html = """
<!doctype html>
<html lang="en">
  <body>
    <main>
      <h1>Release checks</h1>
      <ul id="checks">
        <li data-state="passed">API smoke</li>
        <li data-state="passed">UI smoke</li>
        <li data-state="blocked">Accessibility scan</li>
      </ul>
      <button id="publish">Publish report</button>
    </main>
  </body>
</html>
"""

driver = webdriver.Chrome()
try:
    driver.get("data:text/html;charset=utf-8," + quote(html))

    title = driver.find_element(By.TAG_NAME, "h1")
    checks = driver.find_elements(By.CSS_SELECTOR, "#checks li")
    blocked = driver.find_elements(By.CSS_SELECTOR, "#checks [data-state='blocked']")

    assert title.text == "Release checks"
    assert [check.text for check in checks] == [
        "API smoke",
        "UI smoke",
        "Accessibility scan",
    ]
    assert len(blocked) == 1
finally:
    driver.quit()

The finally block ends the session even if an assertion fails. In pytest, put session creation and quit() in a fixture with yield, but keep the same ownership rule: the code that creates the driver must reliably dispose of it.

3. Understand find_element Return and Failure Behavior

find_element returns the first node matching the locator in the current search context. It does not require the match to be visible, enabled, or unique. If two nodes match, you receive the first. If no node appears within the implicit-wait period, Selenium raises selenium.common.exceptions.NoSuchElementException.

That contract is appropriate for a required control:

username = driver.find_element(By.NAME, "username")
password = driver.find_element(By.NAME, "password")
submit = driver.find_element(By.CSS_SELECTOR, "form#login button[type='submit']")

username.send_keys("qa.user")
password.send_keys("not-a-real-secret")
submit.click()

A returned WebElement is a reference to one DOM node known to the remote end. It is not a Python copy of the element. Properties and methods such as .text, .get_attribute(), .is_displayed(), .click(), and .send_keys() send WebDriver commands. If the UI replaces that node, later commands can raise StaleElementReferenceException.

Required does not always mean immediately present. If a results panel renders after an API response, an immediate finder can race the page. Use an explicit wait and a locator tuple, as shown later. Do not catch NoSuchElementException, sleep, and try again in several unrelated helpers. That recreates a less observable wait mechanism.

Also challenge first-match behavior. If duplicate IDs or duplicate dialog buttons would be a defect, query the collection and assert len(matches) == 1. A singular API is convenient, but it is not a uniqueness assertion.

4. Understand find_elements and Empty Lists

find_elements returns a normal Python list of WebElement references. When no node matches, it returns []. It does not return None and does not raise NoSuchElementException merely because the collection is empty. This makes it a good fit for optional notices and repeated data.

alerts = driver.find_elements(By.CSS_SELECTOR, "[role='alert']")
assert alerts == [], f"Unexpected alerts: {[alert.text for alert in alerts]}"

Python's truthiness makes presence checks compact:

if driver.find_elements(By.CSS_SELECTOR, "[data-testid='cookie-banner']"):
    print("Cookie banner exists in the DOM")

That statement says nothing about visibility. A hidden template or collapsed dialog can still match. If the product rule concerns what the user can see, use a visibility expected condition or filter with is_displayed() after deciding how many hidden and visible copies are allowed.

A list is a result from one command, not a live view. If an infinite-scroll page adds rows, call find_elements again to observe the new DOM. If a framework re-renders rows, references in the old list can become stale. Prefer extracting strings or identifiers after the interface reaches a stable state.

Never index before checking the contract. find_elements(locator)[0] raises IndexError when empty and silently chooses one of many matches. If the first element is truly required, find_element gives a more relevant Selenium failure. If list order is a requirement, assert the count and ordered values before selecting by index.

5. Select Locators That Survive UI Change

A locator should reflect stable product identity. It should not depend on a particular wrapper count, a generated style class, or an English label unless that label is exactly what the test intends to verify.

Locator Python example Good fit Risk to review
ID (By.ID, "search") Stable unique ID Build-generated IDs
Name (By.NAME, "postal_code") Form control contract Same name in several forms
CSS attribute (By.CSS_SELECTOR, "[data-testid='order-row']") Owned test contract Attribute duplicated accidentally
CSS structure (By.CSS_SELECTOR, "#cart tbody tr") Tight component scope Layout coupling if too deep
XPath relation (By.XPATH, "//label[.='Email']/following::input[1]") Relationship not easy in CSS Broad axes can cross components
Link text (By.LINK_TEXT, "Billing") Exact user-facing link Localization or copy edits
Class name (By.CLASS_NAME, "toast") One stable class token Multiple tokens are invalid input
Tag name (By.TAG_NAME, "option") Descendant enumeration Too broad at document scope

Prefer stable IDs and explicit test attributes. CSS is concise for attributes and scoped descendants. XPath is legitimate when relationships or normalized text are the identity, but relative, readable XPath is safer than an absolute /html/body/... chain.

Class name accepts one class token. Use By.CSS_SELECTOR, ".toast.error" when both classes matter. For XPath from a parent element, use .// to keep the expression relative. The dynamic XPath in Selenium Python guide covers quoting, relationships, and changing attributes in more detail.

6. Model Optional, Required, and Unique Elements

Presence helpers often become a source of ambiguity. A generic is_element_present(locator) looks harmless, but callers may use it for required controls, visible state, uniqueness, or disappearance. Build helpers around explicit contracts instead.

from selenium.webdriver.remote.webelement import WebElement

Locator = tuple[str, str]

def find_optional(driver, locator: Locator) -> WebElement | None:
    matches = driver.find_elements(*locator)
    if len(matches) > 1:
        raise AssertionError(f"Expected at most one match for {locator}, got {len(matches)}")
    return matches[0] if matches else None

The *locator syntax expands (By.CSS_SELECTOR, "...") into the two arguments expected by the finder. This helper is appropriate only when zero or one DOM node is a real business state. It does not wait for a dynamic element and does not promise visibility. Its name and type make those limits reviewable.

For exactly one match, use a helper that asserts cardinality with a useful message. For at least one result, state that separately. Avoid broad try: ... except Exception: return False code. It can hide session loss, invalid selectors, stale references, and browser crashes as if the element were merely absent.

A negative check needs timing too. An immediate empty list can pass a millisecond before an unwanted banner appears. If the requirement says a spinner must disappear, wait for invisibility. If it says an error must not appear during a five-second operation, observe the relevant operation and final state instead of relying on one instant query.

7. Wait for State, Then Locate

WebDriverWait repeatedly calls a condition until it returns a truthy value or the timeout expires. The built-in expected_conditions module understands locator tuples and returns useful objects, including a WebElement or list, after the condition succeeds.

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

wait = WebDriverWait(driver, 10)

checkout = wait.until(
    EC.element_to_be_clickable((By.ID, "checkout"))
)
checkout.click()

rows = wait.until(
    EC.visibility_of_all_elements_located(
        (By.CSS_SELECTOR, "table[data-testid='orders'] tbody tr")
    )
)
assert len(rows) >= 1

Choose a condition based on the next operation. presence_of_element_located only needs a DOM node. visibility_of_element_located requires a displayed node with nonzero dimensions. element_to_be_clickable checks visible and enabled. invisibility_of_element_located fits spinners or overlays that must leave. number_of_elements_to_be expresses an exact collection count.

Do not combine a large implicit wait with an explicit wait without measuring the result. Finder calls made during explicit polling can each consume the implicit timeout, making elapsed time unintuitive. Many frameworks use no implicit wait and centralize meaningful explicit waits.

For a custom list rule, let the condition re-run the locator on each poll:

def at_least_three_visible_cards(driver):
    cards = driver.find_elements(By.CSS_SELECTOR, "article.product-card")
    visible = [card for card in cards if card.is_displayed()]
    return visible if len(visible) >= 3 else False

cards = WebDriverWait(driver, 10).until(at_least_three_visible_cards)

Returning the final list avoids an immediate second query after success. For configurable polling and ignored exceptions, see the Selenium fluent wait Python guide.

8. Scope Forms, Cards, Frames, and Shadow DOM

A page can contain several controls with the same label. Search from the smallest stable container to make the locator unambiguous. A sign-in dialog and account page may both contain an email field, but only one belongs to the active flow.

dialog = driver.find_element(By.CSS_SELECTOR, "[role='dialog'][aria-label='Sign in']")
email = dialog.find_element(By.NAME, "email")
buttons = dialog.find_elements(By.TAG_NAME, "button")

assert len(buttons) == 2
email.send_keys("qa@example.com")
dialog.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

Element-scoped searches inspect descendants, not the root element itself. If the root needs validation, call methods on it directly. With XPath, start a relative descendant expression with .//; an expression beginning // can escape the intended scope.

An iframe has its own document. Wait for and switch to it before finding descendants, then return to the default content:

wait.until(EC.frame_to_be_available_and_switch_to_it((By.ID, "payment-frame")))
card_number = wait.until(EC.visibility_of_element_located((By.NAME, "cardnumber")))
card_number.send_keys("4111111111111111")
driver.switch_to.default_content()

For an open shadow root, locate the host, access host.shadow_root, then call find_element or find_elements on that search context. Closed shadow roots are intentionally inaccessible through this route. If a valid selector never matches, verify frame, window, and shadow context before adding wait time.

9. Assert Collections as Domain Data

Directly comparing WebElement objects rarely describes product behavior. Transform the collection into domain data such as product names, order IDs, status values, or normalized prices. Then assertions explain both expected and actual outcomes.

name_elements = driver.find_elements(
    By.CSS_SELECTOR, "[data-testid='search-result'] [data-testid='name']"
)
actual_names = [element.text.strip() for element in name_elements]

assert actual_names, "Expected at least one search result"
assert actual_names == sorted(actual_names, key=str.casefold)
assert len(actual_names) == len(set(actual_names)), "Duplicate product names found"

For a table, build a list of dictionaries. Scope cells within each row so column labels elsewhere cannot match:

rows = driver.find_elements(By.CSS_SELECTOR, "table#orders tbody tr")
orders = [
    {
        "id": row.find_element(By.CSS_SELECTOR, "[data-col='id']").text,
        "status": row.find_element(By.CSS_SELECTOR, "[data-col='status']").text,
    }
    for row in rows
]

assert all(order["status"] in {"Queued", "Running", "Complete"} for order in orders)

Capture values after waiting for the data-loading state to finish. Each .text is a remote call, so a rapidly updating table can change midway through extraction. If the requirement primarily concerns backend data rather than rendering, verify the API at the API layer and keep the UI assertion focused on visible presentation.

Virtualized lists contain only rendered rows. Their DOM count is not the total result count. Scroll and re-query to test virtualization behavior, or assert the total from a separate UI label or API response.

10. Build Page Objects Without Hiding Finder Semantics

A Python page object should offer operations and observable values. Locator tuples are usually better fields than long-lived WebElement objects because a fresh lookup survives component replacement. The object can own its explicit waits and meaningful error messages.

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

class ResultsPage:
    heading = (By.CSS_SELECTOR, "main h1")
    result_cards = (By.CSS_SELECTOR, "[data-testid='result-card']")
    empty_state = (By.CSS_SELECTOR, "[data-testid='empty-results']")

    def __init__(self, driver, timeout: float = 10):
        self.driver = driver
        self.wait = WebDriverWait(driver, timeout)

    def wait_until_loaded(self) -> None:
        self.wait.until(
            lambda d: d.find_elements(*self.result_cards)
            or d.find_elements(*self.empty_state)
        )

    def titles(self) -> list[str]:
        return [
            card.find_element(By.CSS_SELECTOR, "h2").text.strip()
            for card in self.driver.find_elements(*self.result_cards)
        ]

    def visible_heading(self) -> str:
        return self.wait.until(EC.visibility_of_element_located(self.heading)).text

The loading condition accepts either a results collection or an explicit empty state. That is more accurate than waiting only for results, which would time out for a valid zero-result search. In a production page, also distinguish the loading state from an error state so failures are immediate and specific.

Avoid helper methods named get_element that catch everything, return None, take arbitrary sleeps, and conceal the locator. Keep optionality and timing in method names or return types. Page objects should make the product state easier to understand, not reduce every interaction to a generic wrapper.

11. Debug Missing and Stale Elements

A missing element is often a symptom of an earlier failed transition. Before editing the selector, capture the current URL, title, a screenshot, and the presence of a stable page landmark. Check whether a login redirect, consent page, wrong tab, iframe, or API error prevented the expected page from loading.

Catch specific exceptions only where you can add value or recover deliberately:

from selenium.common.exceptions import TimeoutException

try:
    status = wait.until(
        EC.visibility_of_element_located((By.CSS_SELECTOR, "[role='status']"))
    )
except TimeoutException as exc:
    driver.save_screenshot("status-timeout.png")
    raise AssertionError(
        f"Status did not become visible. Current URL: {driver.current_url}"
    ) from exc

Do not catch Exception and continue. A broken session, invalid selector, JavaScript error, and absent optional element are not the same outcome. Preserve the original exception with raise ... from exc when adding domain context.

For stale references, identify the transition that replaced the node. Filtering, sorting, route changes, and auto-refresh are common causes. Wait using a locator so every poll gets the current node. Then interact with or extract from the element returned by the condition. A blind retry decorator around every element method can hide real flicker and produce duplicated clicks.

If local runs pass but CI fails, compare window size, headless behavior, test data, browser version, parallel execution, and response timing. Use focused artifacts. Full page source can contain secrets and creates noisy reports, so redact and limit diagnostic output.

12. Selenium findElement and findElements Python Checklist

Before committing a finder, walk through this checklist:

  1. Translate the API correctly: Python uses find_element and find_elements.
  2. Decide cardinality: must the test find exactly one, zero or one, at least one, or any number?
  3. Decide state: is DOM presence enough, or must the node be visible, enabled, clickable, selected, or absent?
  4. Choose stable identity: prefer owned IDs, attributes, or accessible product semantics over layout positions.
  5. Choose context: document, component root, iframe, window, or open shadow root.
  6. Add explicit synchronization for a real transition, not a fixed sleep.
  7. Re-locate after renders that can replace nodes.
  8. Assert list size before indexing and convert elements into meaningful values.
  9. Include the locator, expected state, and relevant page identity in failure evidence.
  10. End the driver session even when setup, lookup, or assertion fails.

The best code often reads like this: wait for the orders table to show at least one row, retrieve the visible rows, convert them to order data, and assert status rules. The weakest code reads like this: sleep, find everything with a broad class, take index zero, catch all exceptions, and return false. The checklist turns that contrast into repeatable review criteria.

Interview Questions and Answers

Q: What are the Python equivalents of Selenium's findElement and findElements?

They are find_element(by, value) and find_elements(by, value). The Python binding uses snake_case. The singular method returns the first match or raises NoSuchElementException, while the plural method returns a list that can be empty.

Q: Is find_elements a reliable visibility check?

No. It reports matching DOM nodes, including hidden nodes. I use an expected condition for visibility and use find_elements when collection cardinality or optional DOM presence is the actual requirement.

Q: Why store locators as tuples in Python page objects?

A tuple such as (By.ID, "checkout") works with driver.find_element(*locator) and with built-in expected conditions. It keeps strategy and value together and supports a fresh lookup after re-rendering. It is also easy to type, name, and log.

Q: How do you check that an optional modal does not have duplicate copies?

I call find_elements, assert that the length is no greater than one, and return the sole element or None. If visibility matters, I add a separate visible-state rule. This distinguishes valid absence from a duplicate-DOM defect.

Q: What causes StaleElementReferenceException in a list?

The list contains references to exact DOM nodes, and the application replaced or removed those nodes. I wait for the transition with a locator and query the collection again. I avoid caching WebElements across sorting, filtering, or navigation.

Q: How would you wait for three search-result cards?

I use WebDriverWait with number_of_elements_to_be(locator, 3) when exactly three are required. If the rule is at least three visible cards, I write a custom condition that re-queries, filters displayed cards, and returns the list only after its length reaches three.

Q: Why not catch all exceptions in is_element_present?

It can turn invalid selectors, stale references, browser failures, and session loss into a false absence result. I use the empty list for immediate optional presence and catch only a specific timeout or finder exception when the caller's contract justifies it.

Common Mistakes

  • Writing Java camelCase methods in Python or using removed find_element_by_* convenience methods.
  • Using find_elements(...)[0] without asserting the list's intended cardinality.
  • Treating a nonempty list as proof that an element is visible and usable.
  • Returning None for a required control after catching NoSuchElementException.
  • Applying a fixed time.sleep() before every finder instead of waiting for the needed state.
  • Combining a large implicit wait with custom explicit polling and expecting a simple timeout.
  • Storing WebElements as page-object fields across re-renders.
  • Searching globally when a form, dialog, card, or row provides a safer scope.
  • Forgetting to switch into an iframe or back to the default content.
  • Using broad XPath axes, absolute paths, generated classes, or positional CSS as identity.
  • Assuming a virtualized list's DOM row count equals the dataset total.
  • Catching Exception and losing the difference between absence, timeout, bad selector, and dead session.

During review, ask whether each locator call communicates cardinality, context, and state. If one of those is implicit, add a focused assertion or wait before the test becomes difficult to diagnose.

Conclusion

For selenium findElement and findElements python code, remember the real method names and let the expected number of matches drive the choice. Use find_element for a required single node. Use find_elements for collections or genuine optionality, then assert the list rather than assuming its size.

Next, inspect one Python page object for cached elements, global selectors, and ambiguous presence helpers. Replace them with named locator tuples, scoped lookups, and explicit conditions. The result will be more Pythonic, but more importantly, its failures will describe the application state clearly.

Interview Questions and Answers

Compare find_element and find_elements in Selenium Python.

`find_element` returns the first match and raises `NoSuchElementException` for no match. `find_elements` returns all current matches and returns an empty list when none exist. The choice should express whether the UI requires one element or allows a collection or absence.

How do explicit waits integrate with Python locators?

I store locators as `(By, value)` tuples and pass them to expected conditions inside `WebDriverWait.until`. Locator-based conditions re-find the node on each poll and return the successful element or list. This is safer for dynamic DOM updates than waiting on a cached element.

How would you implement a zero-or-one element contract?

I query with `find_elements`, fail if the length exceeds one, and return the only element or `None`. I keep visibility and timing as separate concerns unless the helper name explicitly promises them. This catches duplicate markup without using exceptions for normal absence.

What is wrong with using bool(find_elements(...)) for every check?

It only answers whether a matching DOM node exists at that instant. It does not prove visibility, uniqueness, stability, or that an unwanted node will not appear later. I pair the finder with an assertion or expected condition that matches the actual requirement.

How do you make a collection assertion diagnostic?

I wait for the collection's settled state, convert each element into stable domain values, and compare those values with an informative message. For tables, I build records keyed by identifiers rather than asserting only row indexes. This makes CI output explain the mismatch.

When would you use element-scoped finders?

I use them for repeated components such as product cards, table rows, forms, and dialogs. I first locate the component by stable identity, then locate its descendants. This makes child selectors shorter and prevents matches from unrelated components.

How do you debug NoSuchElementException in CI?

I verify the URL, page landmark, frame or window context, and the preceding state transition before changing the selector. I capture a screenshot and focused redacted diagnostics, then reproduce with matching browser, data, and viewport settings. A longer timeout is justified only if the correct state is genuinely slow.

Frequently Asked Questions

Is it findElement or find_element in Selenium Python?

The current Python method is `find_element`, using snake_case. `findElement` is Java-style terminology and is not a Python WebDriver method.

What happens when find_element finds nothing?

It raises `NoSuchElementException` after any configured implicit wait is exhausted. For a dynamic required element, prefer `WebDriverWait` with a condition that describes the needed state.

What does find_elements return when there are no matches?

It returns an empty Python list. It does not return `None`, so use list length or truthiness according to the product requirement.

How do I pass a locator tuple to find_element?

Store the locator as `(By.CSS_SELECTOR, "value")`, then call `driver.find_element(*locator)`. The asterisk expands the tuple into the `by` and `value` arguments.

Can I find child elements from a WebElement in Python?

Yes. Call `parent.find_element(...)` or `parent.find_elements(...)` to search descendants. This reduces ambiguity in repeated forms, cards, rows, and dialogs.

Should I use find_elements to wait for disappearance?

An immediate empty list is only a point-in-time observation. Use `invisibility_of_element_located` or another explicit condition when disappearance after an operation is the requirement.

Why do elements found in a list become stale?

They reference specific DOM nodes that the page later replaced or removed. Re-query the collection after the UI transition and avoid caching those references.

Related Guides