What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Short answer: In current Selenium Python, replace the deprecated find_elements_by_* call with driver.find_elements(By.STRATEGY, value). An empty list still has several possible causes: an invalid or changed selector, the wrong page or browsing context, asynchronous content that has not entered the DOM, or a lookup made before a click or navigation finished. Fix the API first, then diagnose selector, timing, and context in that order.

Use the Selenium 4 finder API first

The old convenience methods such as find_elements_by_xpath() and find_elements_by_css_selector() are deprecated in current Python Selenium. Import By, pass the strategy as the first argument, and pass the locator value as the second:

from selenium.webdriver.common.by import By

elements = driver.find_elements(By.CSS_SELECTOR, '.result')

Equivalent strategies include By.ID, By.NAME, By.XPATH, By.CSS_SELECTOR, By.CLASS_NAME, By.TAG_NAME, By.LINK_TEXT, and By.PARTIAL_LINK_TEXT. The value must match the strategy. For example, a CSS selector belongs with By.CSS_SELECTOR; an XPath expression belongs with By.XPATH.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Legacy call Current form
driver.find_elements_by_id('login') driver.find_elements(By.ID, 'login')
driver.find_elements_by_name('email') driver.find_elements(By.NAME, 'email')
driver.find_elements_by_xpath('//button') driver.find_elements(By.XPATH, '//button')
driver.find_elements_by_css_selector('.result') driver.find_elements(By.CSS_SELECTOR, '.result')
driver.find_elements_by_class_name('card') driver.find_elements(By.CLASS_NAME, 'card')

Migration alone does not make a wrong selector match. It only ensures that your code uses the supported finder interface.

What an empty list actually means

find_elements is a collection query. It returns a list, including an empty list, when no matching nodes exist in the document and context searched at that instant. It does not tell you whether the selector is wrong, the page is still changing, or the elements are inside another context. A singular call behaves differently: find_element raises NoSuchElementException when no match exists. Invalid CSS or XPath syntax can raise an invalid-selector exception instead of returning an empty collection.

Use the observed result to choose the next diagnostic step rather than adding a random delay.

Diagnose the failure in the order Selenium uses the page

1. Confirm that navigation and the preceding action succeeded

Print the URL and title immediately before the lookup. A redirect, authentication page, consent page, failed form submission, or an exception in an earlier click can leave the driver on a different document than expected.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
print('URL:', driver.current_url)
print('Title:', driver.title)
print('Matches:', driver.find_elements(By.CSS_SELECTOR, '.result'))

Check the rendered DOM with browser developer tools, not just the HTML you remember from an earlier version of the site. Test the selector in the Elements panel and verify that the expected node is in the current document.

2. Validate the selector and its strategy

Keep CSS and XPath syntax separate. These are valid examples:

cards = driver.find_elements(By.CSS_SELECTOR, 'article.card')
links = driver.find_elements(By.XPATH, "//a[contains(@class, 'result-link')]")

Common mistakes include passing .card with By.XPATH, using an XPath where a CSS selector is expected, looking for a class name containing spaces with By.CLASS_NAME, or relying on a generated class that changed after a deployment. Prefer stable IDs, names, data attributes, or a short relationship-based XPath. Avoid selectors tied to presentation-only classes when the application provides a durable attribute.

3. Wait for asynchronous content

Page-load completion and application readiness are different events. The browser can finish loading assets declared in the original HTML while JavaScript is still fetching data or rendering components after a click. A single-page application commonly returns an empty list when the lookup runs immediately after navigation or submission.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use an explicit, condition-based wait. If DOM presence is sufficient, wait for presence; if the next operation requires a visible element, wait for visibility.

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

wait = WebDriverWait(driver, 10)
items = wait.until(
    EC.presence_of_all_elements_located((By.CSS_SELECTOR, '.result'))
)

The timeout should reflect the application and environment. A timeout is useful evidence: it means the condition never became true within the selected interval. It does not prove that changing the find_elements spelling will help.

A fixed time.sleep() can be too short on a slow run and unnecessarily slow on a fast run. It may be useful while exploring a page, but a condition-based wait is the reliable final implementation. Do not combine implicit and explicit waits; their interaction can make polling take longer and produce unpredictable timing.

4. Wait for the event that creates the elements

If results appear only after a click, submit the action and then wait for the result locator. If a previous result list is replaced, wait for an old node to become stale or for a loading indicator to disappear before querying the new list. Re-query after the update instead of holding a list captured before the DOM changed.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
submit = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, 'button[type="submit"]')))
submit.click()
results = wait.until(
    EC.presence_of_all_elements_located((By.CSS_SELECTOR, '.result'))
)

5. Search the correct browsing context

Selenium does not automatically search inside an iframe. Switch to the frame first, locate its contents, then return to the top-level document when finished.

frame = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'iframe.payment')))
driver.switch_to.frame(frame)
fields = driver.find_elements(By.CSS_SELECTOR, 'input')
driver.switch_to.default_content()

For nested frames, switch through each level. A selector that works in the frame will return an empty list from the parent document.

Shadow DOM is another separate search boundary. Locate the host, obtain its shadow root, and search from that root:

host = driver.find_element(By.CSS_SELECTOR, 'user-card')
shadow = host.shadow_root
name = shadow.find_elements(By.CSS_SELECTOR, '.name')

Do not assume that a selector copied from the browser inspector can cross a shadow boundary or reach an iframe without an explicit context switch.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

6. Check for markup changes and driver differences

If the URL, selector, wait, and context are correct, compare the rendered page and behavior in another supported browser. Some failures originate in the underlying browser driver rather than in the locator itself. Keep Selenium, the browser, and its driver aligned with the versions supported in your environment, and isolate a minimal reproduction before changing several variables at once.

A small diagnostic script

This example records the state that matters without hiding a failure behind a broad exception:

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

locator = (By.CSS_SELECTOR, 'article.result')
wait = WebDriverWait(driver, 10)

print('URL:', driver.current_url)
print('Title:', driver.title)

try:
    results = wait.until(EC.presence_of_all_elements_located(locator))
except Exception as exc:
    print('Lookup did not become true:', type(exc).__name__, str(exc))
    print('Current URL:', driver.current_url)
    raise
else:
    print('Result count:', len(results))

Replace the locator with the one you tested in developer tools. During investigation, inspect driver.page_source or a screenshot to determine whether the expected markup exists at all, but do not treat page source as proof that a JavaScript-rendered node is ready for interaction.

Symptom-to-fix checklist

Symptom Likely cause Action
Every locator returns an empty list Wrong URL, redirect, failed prior action, or unexpected document Print URL and title; verify navigation and authentication state.
One selector is empty while nearby elements work Selector mismatch or changed markup Test the exact selector in developer tools and choose a stable attribute.
It works with a long sleep but not without one Asynchronous rendering Replace the sleep with an explicit presence or visibility wait.
Top-level elements are found, frame elements are not Search is occurring outside an iframe Switch to the frame before locating and switch back afterward.
Light-DOM selectors cannot see a component’s children Shadow DOM boundary Locate the host and search from its shadow root.
An exception appears instead of an empty list Invalid selector, stale context, or singular lookup behavior Read the exception type and correct syntax or context before adjusting waits.
Behavior differs between browsers Browser-driver implementation difference Reduce to a minimal case and compare supported browser/driver combinations.

Performance and reliability considerations

  • Use the narrowest stable locator you can maintain. Broad selectors create extra matches and make later assertions ambiguous.
  • Set explicit timeouts according to the page’s normal service time and your CI environment. A very short timeout creates false failures; an unlimited wait hides a real regression.
  • Wait for a meaningful application condition, such as a result node, rather than a generic page-load event.
  • After a framework rerender, locate elements again. Previously returned WebElement objects may no longer represent nodes in the current DOM.
  • Keep frame switching scoped and restore the original context in cleanup code so a later test does not inherit the wrong document.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your goal is a clean visual capture rather than interactive Selenium actions, ScreenshotNeo can return a screenshot or PDF from one request. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing state in X-Page-Verdict and X-Billed headers.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

See the ScreenshotNeo API documentation for all options. A cURL request is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The same request in Python:

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)

And in 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}`);

ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. It is a capture service, not a replacement for Selenium’s clicks, form handling, or frame interaction; use it when you need a repeatable page image or PDF while diagnosing what a rendered page looks like.

The Free plan includes 1,000 screenshots each month with no card. Paid plans start at $5 for 3,000 shots, and every feature is included on every plan. Create a free ScreenshotNeo account to try it.

FAQ

Can an empty list be a successful result?

Yes. If your test is checking that no matching nodes exist, an empty list is the expected collection value. Make that intent explicit in the assertion and separately verify that the page and context are the ones you intended to test.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why did a list become unusable after the page updated?

Frameworks can replace nodes during rerendering. A previously returned WebElement reference may then be stale. Wait for the update and perform a fresh lookup instead of reusing the old reference.

Should I switch to find_element to expose errors?

Use it when exactly one element is required and absence should fail immediately. Keep find_elements when zero, one, or many matches are valid outcomes; changing methods does not fix a selector, timing, or context problem.

Frequently Asked Questions

Can an empty list be a successful result?

Yes. If your test is checking that no matching nodes exist, an empty list is the expected collection value. Make that intent explicit in the assertion and separately verify that the page and context are the ones you intended to test.

Why did a list become unusable after the page updated?

Frameworks can replace nodes during rerendering. A previously returned WebElement reference may then be stale. Wait for the update and perform a fresh lookup instead of reusing the old reference.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Should I switch to find_element to expose errors?

Use it when exactly one element is required and absence should fail immediately. Keep find_elements when zero, one, or many matches are valid outcomes; changing methods does not fix a selector, timing, or context problem.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.