Most “element not found” and “element not interactable” errors in Selenium come down to waiting strategy problems. Covering them up with time.sleep() is a straight path to Flaky Tests. Using WebDriverWait and expected_conditions correctly gives you stable, reliable tests.
📌 Who This Article Is For
✅ Anyone using time.sleep() and wondering if there’s a better way |
✅ Those struggling with NoSuchElementException or ElementNotInteractableException |
✅ Anyone who knows WebDriverWait but wants to organize their expected_conditions usage |
| ✅ Those dealing with Flaky Tests that pass on some runs and fail on others |
📖 What You’ll Be Able to Do After This Guide
| ✔ Understand the differences between implicit waits, explicit waits, and Fluent Wait-style config |
✔ Choose the right expected_conditions method for each situation |
✔ Eliminate time.sleep() and write stable tests |
| ✔ Write custom wait conditions when the built-ins don’t cover your case |
I have worked on browser automation tests using Selenium across multiple real projects.
Waiting strategy problems are one of the most common root causes of Flaky Tests I have encountered.
This guide is a systematic summary of practical waiting patterns that hold up in real-world projects, built from that experience.
✅ Key Takeaways
time.sleep()is off the table. UseWebDriverWait+expected_conditionsinstead- Never mix implicit waits and explicit waits
- Choose the right
expected_conditionsbased on what you’re waiting for
- Why Selenium Needs Waiting Strategies
- Why time.sleep() Is the Wrong Approach
- Three Types of Waits in Selenium
- Using Explicit Waits (WebDriverWait)
- Implicit Waits (implicitly_wait) and Their Caveats
- WebDriverWait with Detailed Config (Fluent Wait-style)
- Writing Custom Wait Conditions
- Integrating WebDriverWait with pytest Fixtures
- 7 Common Waiting Strategy Pitfalls
- Frequently Asked Questions (FAQ)
- Summary
Why Selenium Needs Waiting Strategies
Modern web applications run on asynchronous JavaScript (Ajax, SPAs). When a page first loads, most elements don’t exist yet.
| Common situation | Error that occurs |
|---|---|
| Trying to find an element while the page is still loading | NoSuchElementException |
| Element exists in the DOM but is hidden or disabled | ElementNotInteractableException |
| Another element is animating on top of the target | ElementClickInterceptedException |
| Element was detached from the DOM after a page update | StaleElementReferenceException |
To avoid these errors, you need to wait until elements are ready before interacting with them.
Why time.sleep() Is the Wrong Approach
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get("https://example.com/login") # sample URL
time.sleep(3) # ← this is the problem
driver.find_element(By.ID, "email").send_keys("user@example.com")
| Problem | Real-world impact |
|---|---|
| Wastes time when not needed | If an element appears in 0.5s, you still wait the full 3s — CI slows to a crawl |
| Not enough time in slow environments | On a slow CI day, even 3 seconds isn’t enough and the test fails |
| Breeds Flaky Tests | Pass/fail outcome varies with network speed |
| Doesn’t fix the root cause | Longer sleeps only make tests slower — the underlying problem remains |
Three Types of Waits in Selenium
| Type | How to set it | Recommendation | Use case |
|---|---|---|---|
| Explicit wait | WebDriverWait | 🔴 Recommended | Wait until a specific condition is met |
| Implicit wait | implicitly_wait() | ⚠️ Limited use | Globally wait for element DOM appearance |
| Fluent Wait-style config | WebDriverWait (detailed) | 🟡 Advanced | Fine-tune polling interval and ignored exceptions |
Using Explicit Waits (WebDriverWait)
An explicit wait says “wait up to N seconds until this specific condition is met.” As soon as the condition is satisfied, execution continues immediately — no unnecessary waiting.
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
driver = webdriver.Chrome()
driver.get("https://example.com/login") # sample URL
# Basic WebDriverWait pattern:
# WebDriverWait(driver, timeout_seconds).until(condition)
wait = WebDriverWait(driver, 10) # wait up to 10 seconds
# Wait until input field is visible (for text inputs)
email_input = wait.until(
EC.visibility_of_element_located((By.ID, "email"))
)
email_input.send_keys("user@example.com")
# Wait until button is visible AND enabled (clickable)
submit_btn = wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-testid='login-button']"))
)
submit_btn.click()
expected_conditions — Key Methods Reference
| Method | What it waits for | When to use it |
|---|---|---|
presence_of_element_located | Element exists in the DOM (can be hidden) | Reading text or attribute values |
visibility_of_element_located | Element is visible (size > 0) | Filling inputs, visibility checks, screenshots |
element_to_be_clickable | Element is visible AND enabled | Clicking buttons (most commonly used) |
invisibility_of_element_located | Element has become hidden | Waiting for a loading spinner to disappear |
text_to_be_present_in_element | Element’s text contains the specified string | Waiting for “Saved successfully” or similar messages |
url_contains | URL contains the specified string | Waiting for a page navigation to complete |
url_to_be | URL exactly matches the specified value | Confirming the post-login redirect destination |
presence_of_all_elements_located | At least one element matches the locator | Waiting for a list or table to load |
staleness_of | Element has been detached from the DOM | Waiting for element refresh after page reload |
alert_is_present | An alert dialog has appeared | Before handling a confirm or alert dialog |
Code Examples by Situation
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
wait = WebDriverWait(driver, 10)
# ① Click a button (most common)
btn = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-testid='submit-btn']")))
btn.click()
# ② Wait for a loading spinner to disappear
wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, ".loading-spinner")))
# ③ Wait for a success message to appear
wait.until(EC.text_to_be_present_in_element(
(By.CSS_SELECTOR, "[data-testid='status-message']"),
"Saved successfully"
))
# ④ Wait for page navigation
wait.until(EC.url_contains("/dashboard"))
# ⑤ Wait for a list to load (at least one item)
items = wait.until(
EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".item-list li"))
)
assert len(items) > 0
# ⑥ Handle an alert dialog
wait.until(EC.alert_is_present())
alert = driver.switch_to.alert
alert.accept()
Implicit Waits (implicitly_wait) and Their Caveats
Implicit waits set a global timeout applied to every find_element call. Configure it once and it applies automatically to all element lookups.
driver = webdriver.Chrome()
driver.implicitly_wait(10) # wait up to 10s on every find_element
driver.get("https://example.com/login") # sample URL
# All subsequent find_element calls wait up to 10 seconds
email_input = driver.find_element(By.ID, "email")
Using both simultaneously causes unpredictable behavior — wait times can compound or conflict.
In larger projects, teams typically standardize on explicit waits (
WebDriverWait). Pick one approach and stick to it.WebDriverWait with Detailed Config (Fluent Wait-style)
This is a more detailed configuration of WebDriverWait where you fine-tune the polling interval and ignored exceptions.
Note that Selenium Python does not have a separate FluentWait class like Java does — this behavior is achieved by adjusting timeout, poll_frequency, and ignored_exceptions on WebDriverWait.
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException
# Detailed WebDriverWait configuration
wait = WebDriverWait(
driver,
timeout=15, # maximum wait time in seconds
poll_frequency=0.5, # check interval in seconds (default: 0.5s)
ignored_exceptions=[ # exceptions to ignore (keep waiting if these occur)
NoSuchElementException,
StaleElementReferenceException,
]
)
element = wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-testid='dynamic-btn']"))
)
element.click()
Plain
WebDriverWait handles most situations. The detailed config is useful when:- You want to fine-tune the polling interval for responsiveness
StaleElementReferenceExceptionis happening frequently on a dynamic page- You want to suppress specific exceptions during the wait
Writing Custom Wait Conditions
When the built-in expected_conditions don’t cover your case, you can define a custom condition as a function or class.
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
# ① Simple lambda approach
wait = WebDriverWait(driver, 10)
# Wait until an element's attribute changes to the expected value
wait.until(lambda d: d.find_element(By.ID, "status").get_attribute("data-state") == "loaded")
# ② Reusable class approach
class element_has_class:
"""Wait until the specified element has a particular CSS class."""
def __init__(self, locator: tuple, css_class: str) -> None:
self.locator = locator
self.css_class = css_class
def __call__(self, driver):
element = driver.find_element(*self.locator)
return self.css_class in element.get_attribute("class")
# Usage: wait until the button has the "active" class
wait.until(element_has_class(
(By.CSS_SELECTOR, "[data-testid='submit-btn']"),
"active"
))
Integrating WebDriverWait with pytest Fixtures
In real projects, wrapping WebDriverWait in a pytest fixture keeps test code clean and consistent.
# conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
@pytest.fixture
def driver():
d = webdriver.Chrome()
d.implicitly_wait(0) # explicitly disable implicit waits
yield d
d.quit()
@pytest.fixture
def wait(driver):
return WebDriverWait(driver, 10)
# test_login.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
def test_login_success(driver, wait) -> None:
driver.get("https://example.com/login") # sample URL
wait.until(EC.visibility_of_element_located((By.ID, "email"))).send_keys("user@example.com")
wait.until(EC.visibility_of_element_located((By.ID, "password"))).send_keys("password123")
wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-testid='login-btn']"))).click()
wait.until(EC.url_contains("/dashboard"))
assert "/dashboard" in driver.current_url
- Selenium + Python Setup — From WebDriver Config to Your First Test
- Selenium × pytest Practical Guide — Fixtures, Parametrize & conftest.py
- CSS Selectors & XPath — Understanding Locators in Selenium & Playwright
- Fixing Flaky Tests — A Practical Guide from Root Cause to Resolution
- pytest Fixture Guide — yield, scope & conftest.py Explained
7 Common Waiting Strategy Pitfalls
⚠️ Watch out for these
- Using presence_of_element_located before clicking and getting an error
presence_of_element_locatedonly confirms the element exists in the DOM — it doesn’t check visibility or enabled state. Useelement_to_be_clickablewhen you need to click. - Mixing implicit waits and explicit waits
Using both causes wait times to compound or conflict unpredictably. Standardize on one — explicit waits (WebDriverWait) are the recommended choice. - TimeoutException fires without a clear diagnosis
WhenWebDriverWaittimes out, it raisesTimeoutException. Read the error message to understand what condition was being waited for. Taking a screenshot at failure helps too.from selenium.common.exceptions import TimeoutException try: wait.until(EC.element_to_be_clickable((By.ID, "submit"))) except TimeoutException: driver.save_screenshot("timeout_debug.png") raise - Accessing a stale element after a page update → StaleElementReferenceException
Elements fetched before an Ajax update or page reload become stale immediately afterward. Usestaleness_ofto wait for the old element to disappear, then re-fetch — or redesign the test to always callfind_elementfresh. - Clicking an element while an animation is playing → ElementClickInterceptedException
Modal animations or tooltips can sit on top of the target and block clicks. The root cause fix is to check your locator, waiting condition, and any overlay elements first.ActionChainsor JavaScript click (driver.execute_script("arguments[0].click();", element)) can sometimes work around it, but they mask the real problem — treat them as a last resort. - Using the same timeout value for everything
Page navigation (5–10s), API responses (3–5s), and animations (1–2s) each have different appropriate timeouts. A blanket 10s for everything produces unnecessarily slow tests. - Not waiting after driver.get()
driver.get()waits untildocument.readyState == "complete", but that doesn’t cover JavaScript-rendered content. Always useWebDriverWaitafter page navigation before interacting with dynamically loaded elements.
Frequently Asked Questions (FAQ)
The default polling interval is 0.5 seconds — meaning Selenium checks whether the condition is satisfied every 500ms. You can adjust this with the poll_frequency parameter (e.g. WebDriverWait(driver, 10, poll_frequency=0.2)). More frequent checks improve responsiveness but also increase browser load.
Using a pytest fixture is the simplest approach (see the “pytest fixture integration” section above). In larger projects, the common pattern is to put wait in a Page Object Model (POM) BasePage class so every page object can use it. Managing the timeout value as a constant in conftest.py makes it easy to adjust across the whole test suite.
A general guideline: 5–10 seconds for typical waits, 10–15 seconds for network-dependent ones. CI environments are often slower than local machines, so leave some headroom. That said, longer timeouts mean slower tests — measure your actual response times and set values accordingly.
Avoid it in general. That said, there are rare practical exceptions — such as animation completion waits where the duration is fixed and condition-based detection is impractical. In those cases, a short sleep (e.g. 0.3s) can be a pragmatic choice. Treat it as an absolute last resort after exhausting expected_conditions and custom condition options.
until(condition) waits until the condition returns True. until_not(condition) waits until it returns False. To wait for a loading spinner to disappear, use until(EC.invisibility_of_element_located(...)) — or equivalently, until_not(EC.visibility_of_element_located(...)).
Playwright ships with auto-waiting built in, so explicit wait setup like Selenium’s WebDriverWait is rarely needed. page.click(), page.fill(), and other action methods automatically wait for the element to be actionable. For cases where you do need to wait on a specific condition, page.wait_for_selector() and page.wait_for_url() are available.
NoSuchElementException fires the instant find_element is called and the element isn’t found. TimeoutException fires when a WebDriverWait condition isn’t satisfied within the timeout period. Flaky Tests typically produce TimeoutException — review your wait condition and timeout value when that happens.
Summary
| Key point | What to remember |
|---|---|
| No time.sleep() | It wastes time and breeds Flaky Tests |
| Use explicit waits | WebDriverWait(driver, 10).until(condition) is the base pattern |
| Choose the right EC | Clicking → element_to_be_clickable / Filling inputs → visibility_of_element_located / Waiting to disappear → invisibility_of_element_located |
| Don’t mix wait types | Implicit and explicit waits together cause unpredictable behavior |
| Detailed config (Fluent Wait-style) | Tune poll_frequency and ignored_exceptions when needed — no separate FluentWait class exists in Python |
| Custom conditions | Use lambda or a class to define your own wait logic |
| Use pytest fixtures | Wrap WebDriverWait in a fixture for consistent reuse across tests |
Writing waits correctly is one of the most effective things you can do to improve Selenium test stability.
A good first step: search your existing test code for time.sleep() and replace each one with the appropriate element_to_be_clickable or visibility_of_element_located.
In practice, tracing the root cause of mysterious Flaky Tests almost always leads to one of three things: the wait is insufficient, the condition is wrong, or the locator is broken. Reviewing your waiting strategy is the fastest path to fixing Flaky Tests.
GitHub Actions, GitLab CI, and similar environments are sometimes slower than your local machine. If tests that pass locally hit
TimeoutException in CI, try increasing the timeout (e.g. 10s → 15s) or managing the value in a CI-specific configuration file.Selenium vs Playwright: Different Philosophies on Waiting
| Aspect | Selenium | Playwright |
|---|---|---|
| Core philosophy | Explicit wait-centric (write it yourself) | Auto-waiting centric (built in) |
| Default behavior | No wait (immediate lookup) | Waits automatically before each action |
| Risk of time.sleep() creep | △ Common trap | ○ Much less common |
| Custom waits | WebDriverWait + EC / lambda | expect() / wait_for_selector() |
Getting Started with Playwright|Browser Automation with Python — A Beginner’s Guide
