Explicit Wait in Selenium: WebDriverWait Complete Guide (2026)
Explicit wait in Selenium uses WebDriverWait(driver, timeout).until(condition) to pause execution until a specific condition is met. Use expected_conditions (EC) for common conditions: EC.presence_of_element_located, EC.visibility_of_element_located, EC.element_to_be_clickable. Never mix explicit and implicit waits — use explicit waits exclusively.
Key Takeaways
Explicit wait is the only correct wait strategy. Implicit waits apply globally and interact unpredictably with explicit waits. time.sleep() wastes time. Use explicit waits with specific conditions for every element interaction.
Choose the right expected condition. presence_of_element_located (element in DOM), visibility_of_element_located (element visible), element_to_be_clickable (visible + enabled). Wrong condition = flaky tests.
Poll interval defaults to 500ms. WebDriverWait checks the condition every 500ms. For faster checks, set poll_frequency=0.1. For slow elements, increase the timeout, not the poll frequency.
TimeoutException tells you what went wrong. Always add descriptive messages: WebDriverWait(..., message="Checkout button not clickable after 10s"). This saves debugging time.
Custom conditions handle complex waiting logic. When built-in conditions aren't enough, write a lambda or class-based condition. For example, wait for element text to match a regex.
What is Explicit Wait in Selenium?
Explicit wait tells Selenium: "Wait up to N seconds for this specific condition to be true, polling every 500ms."
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
wait = WebDriverWait(driver, 10) # Max 10 seconds
element = wait.until(EC.element_to_be_clickable((By.ID, "submit-btn")))
element.click()
Compare this to the bad alternatives:
# ❌ time.sleep — always waits the full duration, even if element appears in 0.5s
import time
time.sleep(5)
element = driver.find_element(By.ID, "submit-btn")
# ❌ implicit wait — global, interacts badly with explicit waits
driver.implicitly_wait(10) # Set once, applies to ALL find_element calls
WebDriverWait Parameters
from selenium.webdriver.support.ui import WebDriverWait
wait = WebDriverWait(
driver=driver,
timeout=10, # Max seconds to wait
poll_frequency=0.5, # Check every 0.5 seconds (default)
ignored_exceptions=[ # Don't fail on these exceptions
NoSuchElementException,
StaleElementReferenceException,
]
)
Common configurations:
# Fast-polling wait (for responsive apps)
fast_wait = WebDriverWait(driver, 5, poll_frequency=0.1)
# Long wait for slow operations
slow_wait = WebDriverWait(driver, 30)
# Wait that ignores stale elements
stable_wait = WebDriverWait(
driver, 10,
ignored_exceptions=[StaleElementReferenceException]
)
Expected Conditions Reference
Presence and Visibility
from selenium.webdriver.support import expected_conditions as EC
# Element exists in DOM (may be hidden)
EC.presence_of_element_located((By.ID, "element"))
# Element exists and is visible (display != none, visibility != hidden)
EC.visibility_of_element_located((By.ID, "element"))
# Element is visible (pass the WebElement directly)
EC.visibility_of(already_found_element)
# Element is NOT visible (hidden or removed)
EC.invisibility_of_element_located((By.ID, "loading-spinner"))
# All elements matching selector are present
EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".list-item"))
Clickability and Interactivity
# Element is visible and enabled (ready to click)
EC.element_to_be_clickable((By.ID, "submit-btn"))
# Element is selected (checkbox/radio)
EC.element_to_be_selected(element)
# Element is NOT selected
EC.element_located_to_be_selected((By.ID, "checkbox"))
URL and Title
# URL equals exactly
EC.url_to_be("https://example.com/dashboard")
# URL contains substring
EC.url_contains("/dashboard")
# URL matches regex
EC.url_matches(r".*\/dashboard\/\d+")
# Title equals exactly
EC.title_is("Dashboard - MyApp")
# Title contains substring
EC.title_contains("Dashboard")
Text Content
# Element's text equals
EC.text_to_be_present_in_element((By.ID, "status"), "Complete")
# Element's text contains substring
EC.text_to_be_present_in_element((By.ID, "counter"), "items")
# Input value contains text
EC.text_to_be_present_in_element_value((By.ID, "search"), "query")
Frames and Windows
# Wait for iframe and switch into it
EC.frame_to_be_available_and_switch_to_it((By.ID, "payment-iframe"))
EC.frame_to_be_available_and_switch_to_it(0) # By index
# New browser tab/window opened
EC.new_window_is_opened(current_handles)
# Number of windows equals N
EC.number_of_windows_to_be(2)
Alert
# JavaScript alert is present
EC.alert_is_present()
Element State
# Element exists and is stale (DOM was refreshed)
EC.staleness_of(element)
# Element has specific attribute value
EC.element_attribute_to_include((By.ID, "btn"), "class")
Practical Examples
Wait for Page Load After Navigation
driver.get("https://example.com/products")
# Wait for a key element that confirms the page loaded
product_list = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".product-grid"))
)
Wait for AJAX Content
# Click a button that loads data asynchronously
driver.find_element(By.ID, "load-more").click()
# Wait for new items to appear
items = wait.until(
EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".product-card"))
)
assert len(items) > 0
Wait for Loading Spinner to Disappear
# Trigger an action
driver.find_element(By.ID, "save-button").click()
# Wait for spinner to appear (optional, confirms request started)
wait.until(EC.visibility_of_element_located((By.ID, "loading-spinner")))
# Wait for spinner to disappear
wait.until(EC.invisibility_of_element_located((By.ID, "loading-spinner")))
# Now interact with the result
success_message = wait.until(
EC.visibility_of_element_located((By.CSS_SELECTOR, ".success-toast"))
)
assert "saved" in success_message.text.lower()
Wait for Modal to Open
# Click button that opens modal
driver.find_element(By.ID, "delete-btn").click()
# Wait for modal
modal = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".modal")))
# Click confirm inside modal
confirm_btn = wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR, ".modal .btn-danger"))
)
confirm_btn.click()
# Wait for modal to close
wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, ".modal")))
Wait for URL Change After Redirect
# Submit form
driver.find_element(By.ID, "submit").click()
# Wait for redirect
wait.until(EC.url_contains("/success"))
# or
wait.until(EC.url_to_be("https://example.com/order/12345/success"))
Wait for Text to Update
# Trigger update
driver.find_element(By.ID, "refresh-status").click()
# Wait for status text to change
wait.until(
EC.text_to_be_present_in_element((By.ID, "order-status"), "Shipped")
)
Custom Wait Conditions
When built-in conditions aren't enough, write custom ones:
Lambda Condition
# Wait for element count to reach a specific number
wait.until(lambda d: len(d.find_elements(By.CSS_SELECTOR, ".item")) >= 10)
# Wait for JavaScript variable to be set
wait.until(lambda d: d.execute_script("return window.appReady === true"))
# Wait for element to have a specific CSS class
wait.until(lambda d: "active" in d.find_element(By.ID, "tab").get_attribute("class"))
# Wait for element attribute value
wait.until(
lambda d: d.find_element(By.ID, "progress").get_attribute("value") == "100"
)
Class-Based Condition
For reusable, parameterizable conditions:
class element_has_class:
"""Wait for element to have a specific CSS class."""
def __init__(self, locator, css_class):
self.locator = locator
self.css_class = css_class
def __call__(self, driver):
try:
element = driver.find_element(*self.locator)
return self.css_class in element.get_attribute("class")
except:
return False
class element_count_exceeds:
"""Wait for element count to exceed a threshold."""
def __init__(self, locator, count):
self.locator = locator
self.count = count
def __call__(self, driver):
elements = driver.find_elements(*self.locator)
return len(elements) > self.count
# Usage
wait.until(element_has_class((By.ID, "submit-btn"), "btn-primary"))
wait.until(element_count_exceeds((By.CSS_SELECTOR, ".result-item"), 5))
Error Handling
TimeoutException
from selenium.common.exceptions import TimeoutException
try:
element = wait.until(EC.visibility_of_element_located((By.ID, "result")))
element.click()
except TimeoutException:
print(f"Element not found after {10}s")
print(f"Current URL: {driver.current_url}")
# Take screenshot for debugging
driver.save_screenshot("timeout_debug.png")
raise
Adding Descriptive Messages
# WebDriverWait accepts a message parameter for better error output
from selenium.webdriver.support.ui import WebDriverWait
wait = WebDriverWait(driver, 10)
element = wait.until(
EC.element_to_be_clickable((By.ID, "checkout-button")),
message="Checkout button was not clickable after 10 seconds on checkout page"
)
When this times out, the error message will include your description — much easier to debug than a generic TimeoutException.
Explicit Wait vs Implicit Wait vs FluentWait
| Feature | Explicit Wait | Implicit Wait | Fluent Wait |
|---|---|---|---|
| Scope | Per-element | Global | Per-element |
| Condition | Specific EC | Element exists | Custom |
| Poll frequency | Configurable | Browser default | Configurable |
| Exception handling | Configurable | NoSuchElementException | Configurable |
| Reliability | High | Low | High |
| Recommendation | ✅ Use this | ❌ Avoid | ✅ For complex cases |
Never mix implicit and explicit waits. When you set driver.implicitly_wait(10) and also use WebDriverWait, behavior becomes unpredictable. Some waits compound, some don't.
# ❌ Never do this
driver.implicitly_wait(10)
wait = WebDriverWait(driver, 10)
element = wait.until(EC.presence_of_element_located((By.ID, "element")))
# ✅ Do this — explicit waits only
driver.implicitly_wait(0) # Disable implicit waits
wait = WebDriverWait(driver, 10)
element = wait.until(EC.presence_of_element_located((By.ID, "element")))
Common Mistakes
Mistake 1: Using presence when you need visibility
# ❌ Element might be hidden
element = wait.until(EC.presence_of_element_located((By.ID, "modal-content")))
element.click() # ElementNotInteractableException — element is hidden!
# ✅ Wait for visibility
element = wait.until(EC.visibility_of_element_located((By.ID, "modal-content")))
element.click()
Mistake 2: Short timeout on slow actions
# ❌ 2 seconds too short for page load
wait = WebDriverWait(driver, 2)
element = wait.until(EC.url_contains("/dashboard"))
# ✅ Use appropriate timeout
wait = WebDriverWait(driver, 15)
element = wait.until(EC.url_contains("/dashboard"))
Mistake 3: Waiting after already finding the element
# ❌ Find element, THEN wait — race condition if DOM updates
element = driver.find_element(By.ID, "status")
wait.until(EC.text_to_be_present_in_element(element, "Done"))
# ✅ Pass locator tuple, not element
wait.until(EC.text_to_be_present_in_element((By.ID, "status"), "Done"))
Mistake 4: Reusing WebDriverWait after timeout
# ❌ After TimeoutException, the wait object is exhausted
try:
wait.until(EC.visibility_of_element_located((By.ID, "slow-element")))
except TimeoutException:
pass
# Reusing wait here may not start a fresh 10-second window
# ✅ Create a new WebDriverWait instance for each critical wait
Full Test Example with Explicit Waits
import pytest
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
from selenium.common.exceptions import TimeoutException
def test_purchase_flow(driver):
wait = WebDriverWait(driver, 10)
# 1. Load product page
driver.get("https://shop.example.com/product/123")
wait.until(EC.title_contains("Product"))
# 2. Add to cart
add_to_cart = wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-testid='add-to-cart']"))
)
add_to_cart.click()
# 3. Wait for cart count to update
wait.until(
EC.text_to_be_present_in_element((By.ID, "cart-count"), "1")
)
# 4. Go to checkout
checkout_btn = wait.until(
EC.element_to_be_clickable((By.ID, "checkout-button"))
)
checkout_btn.click()
wait.until(EC.url_contains("/checkout"))
# 5. Fill shipping info
name = wait.until(EC.visibility_of_element_located((By.ID, "full-name")))
name.send_keys("Test User")
# 6. Submit order
place_order = wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR, "button[data-testid='place-order']"))
)
place_order.click()
# 7. Wait for confirmation
wait = WebDriverWait(driver, 30) # Longer wait for payment processing
wait.until(EC.url_contains("/confirmation"))
order_number = wait.until(
EC.visibility_of_element_located((By.ID, "order-number"))
)
assert order_number.text.startswith("ORD-")
The Alternative: AI Handles Waits Automatically
Every Selenium test requires explicit wait logic. Miss one wait and you get flaky tests. HelpMeTest removes this entirely:
Go to https://shop.example.com/product/123
Click the "Add to Cart" button
Verify the cart shows 1 item
Click "Checkout"
Fill "Test User" in the name field
Click "Place Order"
Verify an order confirmation number is shown
The AI waits for elements automatically — it won't click until the element is actually interactive, just like a human. No WebDriverWait, no expected_conditions, no TimeoutException debugging.
Explicit waits remain essential when:
- Maintaining large existing Selenium codebases
- Testing complex timing scenarios specifically
- Need precise control over wait conditions
- Framework or tooling requires Selenium
When to reconsider:
- Spending more time debugging flaky waits than writing tests
- Non-technical team members need to write tests
- Tests break after every UI update
Summary
Explicit wait best practices:
- Always use
WebDriverWait+expected_conditions— nevertime.sleep() - Disable implicit waits:
driver.implicitly_wait(0) - Choose the right EC:
presence<visibility<element_to_be_clickable - Add descriptive
messageparameters for easier debugging - Use custom lambdas for conditions not covered by EC
- Match timeout to actual operation speed (10s for UI, 30s for slow APIs)