Selenium WebDriverWait: Complete Python Guide with Examples (2026)
WebDriverWait is Selenium's explicit wait class. Import it with from selenium.webdriver.support.ui import WebDriverWait. Use it as WebDriverWait(driver, timeout).until(condition) where condition is an expected_conditions function or a callable. It polls every 500ms until the condition is true or timeout is reached.
Key Takeaways
WebDriverWait takes two required parameters: driver and timeout (in seconds). WebDriverWait(driver, 10) waits up to 10 seconds.
.until() and .until_not() are the key methods. .until(condition) waits for condition to return a truthy value. .until_not(condition) waits for condition to return a falsy value (useful for waiting for spinners to disappear).
Pair with expected_conditions (EC) for common scenarios. EC.element_to_be_clickable, EC.visibility_of_element_located, EC.presence_of_element_located cover 90% of cases.
WebDriverWait does NOT set a global timeout. Each WebDriverWait(driver, N) instance is independent. Unlike driver.implicitly_wait(), it only applies to the specific .until() call.
Never mix with driver.implicitly_wait(). Both active simultaneously causes undefined behavior. Disable implicit wait: driver.implicitly_wait(0).
WebDriverWait Syntax
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
# Basic usage
element = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.ID, "submit-button"))
)
element.click()
Constructor Parameters
WebDriverWait(
driver, # Required: WebDriver instance
timeout, # Required: Max seconds to wait (int or float)
poll_frequency=0.5, # Optional: Seconds between checks (default 0.5)
ignored_exceptions=None, # Optional: List of exceptions to ignore while polling
)
timeout
The maximum time in seconds to wait before raising TimeoutException:
# Common timeout values
quick_wait = WebDriverWait(driver, 3) # 3s — fast UI interactions
wait = WebDriverWait(driver, 10) # 10s — standard page loads
slow_wait = WebDriverWait(driver, 30) # 30s — slow API calls, file uploads
poll_frequency
How often to check the condition. Default is 0.5 seconds (500ms):
# Check every 100ms for faster responses
fast_wait = WebDriverWait(driver, 5, poll_frequency=0.1)
# Check every 2 seconds for slow polling (saves CPU on long waits)
long_wait = WebDriverWait(driver, 120, poll_frequency=2)
ignored_exceptions
Exceptions to swallow during polling. By default, WebDriverWait ignores NoSuchElementException. Add StaleElementReferenceException when the DOM refreshes:
from selenium.common.exceptions import (
NoSuchElementException,
StaleElementReferenceException,
ElementClickInterceptedException,
)
stable_wait = WebDriverWait(
driver, 10,
ignored_exceptions=[
NoSuchElementException,
StaleElementReferenceException,
]
)
The .until() Method
.until(method) calls method(driver) repeatedly until it returns a truthy value:
# Wait for element to be clickable — returns the element when ready
button = wait.until(EC.element_to_be_clickable((By.ID, "submit")))
# Wait for URL to change — returns True when condition is met
wait.until(EC.url_contains("/dashboard"))
# Wait for element count — returns list of elements when count is reached
items = wait.until(lambda d: d.find_elements(By.CSS_SELECTOR, ".item") if len(d.find_elements(By.CSS_SELECTOR, ".item")) >= 5 else False)
The .until_not() Method
.until_not(method) waits for condition to return False or None:
# Wait for spinner to disappear
wait.until_not(EC.visibility_of_element_located((By.CSS_SELECTOR, ".loading-spinner")))
# Wait for modal to close
wait.until_not(EC.presence_of_element_located((By.CSS_SELECTOR, ".modal")))
# Wait for button to be disabled
wait.until_not(EC.element_to_be_clickable((By.ID, "submit-btn")))
Expected Conditions Reference
Import with from selenium.webdriver.support import expected_conditions as EC.
Element Location and Visibility
# Element in DOM (may be hidden)
EC.presence_of_element_located((By.ID, "element"))
# Element visible on page
EC.visibility_of_element_located((By.ID, "element"))
# Element visible (pass WebElement directly)
EC.visibility_of(already_found_element)
# Element not visible or not in DOM
EC.invisibility_of_element_located((By.CSS_SELECTOR, ".spinner"))
# All elements matching selector are in DOM
EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".list-item"))
# All elements matching selector are visible
EC.visibility_of_all_elements_located((By.CSS_SELECTOR, ".grid-item"))
# At least one element matching selector is visible
EC.visibility_of_any_elements_located((By.CSS_SELECTOR, ".notification"))
Clickability and Interactivity
# Visible and enabled — ready to click
EC.element_to_be_clickable((By.ID, "submit"))
# Element is selected (checkbox or radio button)
EC.element_to_be_selected(element)
# Element selection state (True/False)
EC.element_selection_state_to_be(element, True)
# Element located and selected
EC.element_located_to_be_selected((By.ID, "checkbox"))
# Element located with specific selection state
EC.element_located_selection_state_to_be((By.ID, "checkbox"), True)
URL and Title
# Exact URL match
EC.url_to_be("https://example.com/dashboard")
# URL contains substring
EC.url_contains("/dashboard")
# URL matches regex
EC.url_matches(r"https://example\.com/orders/\d+")
# URL changes from current
EC.url_changes("https://example.com/login")
# Title exact match
EC.title_is("Dashboard - MyApp")
# Title contains substring
EC.title_contains("Dashboard")
Text Content
# Element text contains substring
EC.text_to_be_present_in_element((By.ID, "status"), "Complete")
# Input value contains text
EC.text_to_be_present_in_element_value((By.ID, "search"), "query")
# Element attribute contains text
EC.text_to_be_present_in_element_attribute((By.ID, "btn"), "class", "active")
Frames and Windows
# Switch to iframe and wait for it
EC.frame_to_be_available_and_switch_to_it((By.ID, "payment-frame"))
EC.frame_to_be_available_and_switch_to_it(0) # By index
# New window/tab opened
EC.new_window_is_opened(driver.window_handles)
# Exact number of windows
EC.number_of_windows_to_be(2)
Staleness and Alerts
# Wait for element to become stale (DOM refreshed)
EC.staleness_of(old_element)
# Alert is present
EC.alert_is_present()
Practical Examples
Login Flow
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.implicitly_wait(0) # Disable implicit wait
wait = WebDriverWait(driver, 10)
driver.get("https://example.com/login")
# Wait for form to load
username_field = wait.until(EC.visibility_of_element_located((By.ID, "username")))
username_field.send_keys("testuser@example.com")
password_field = driver.find_element(By.ID, "password")
password_field.send_keys("securepassword")
submit = wait.until(EC.element_to_be_clickable((By.ID, "login-btn")))
submit.click()
# Wait for successful navigation
wait.until(EC.url_contains("/dashboard"))
wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".user-avatar")))
AJAX Content Loading
# Click "Load More" button
load_more = wait.until(EC.element_to_be_clickable((By.ID, "load-more")))
initial_count = len(driver.find_elements(By.CSS_SELECTOR, ".product-card"))
load_more.click()
# Wait for new items to appear
wait.until(lambda d: len(d.find_elements(By.CSS_SELECTOR, ".product-card")) > initial_count)
# Get new items
products = driver.find_elements(By.CSS_SELECTOR, ".product-card")
Form Submission with Spinner
# Submit form
submit = wait.until(EC.element_to_be_clickable((By.ID, "submit-form")))
submit.click()
# Wait for spinner to appear (confirms request started)
wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".processing-spinner")))
# Wait for spinner to disappear (request complete)
slow_wait = WebDriverWait(driver, 30)
slow_wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, ".processing-spinner")))
# Check result
success = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".success-message")))
assert "submitted successfully" in success.text
Navigating Between Pages
# Click navigation link
nav_link = wait.until(EC.element_to_be_clickable((By.LINK_TEXT, "Settings")))
nav_link.click()
# Wait for URL change
wait.until(EC.url_contains("/settings"))
# Wait for page content
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".settings-panel")))
Waiting for Modal
# Click button to open modal
open_modal_btn = wait.until(EC.element_to_be_clickable((By.ID, "open-dialog")))
open_modal_btn.click()
# Wait for modal
modal = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".modal-dialog")))
# Interact inside modal
confirm_btn = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, ".modal-dialog .btn-confirm")))
confirm_btn.click()
# Wait for modal to close
wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, ".modal-dialog")))
Custom Wait Conditions
When built-in expected_conditions aren't enough:
Lambda Conditions
# Wait for element count to reach 10
wait.until(lambda d: len(d.find_elements(By.CSS_SELECTOR, ".item")) == 10)
# Wait for JavaScript value
wait.until(lambda d: d.execute_script("return window.pageLoaded === true"))
# Wait for element CSS class
wait.until(
lambda d: "active" in d.find_element(By.ID, "tab-1").get_attribute("class")
)
# Wait for element attribute value
wait.until(
lambda d: d.find_element(By.ID, "progress-bar").get_attribute("aria-valuenow") == "100"
)
Class-Based Conditions (Reusable)
class element_has_css_class:
"""Wait for element to acquire a 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)
classes = element.get_attribute("class").split()
return element if self.css_class in classes else False
except:
return False
class element_count_is:
"""Wait for exact count of elements."""
def __init__(self, locator, count):
self.locator = locator
self.count = count
def __call__(self, driver):
elements = driver.find_elements(*self.locator)
return elements if len(elements) == self.count else False
# Usage
wait.until(element_has_css_class((By.ID, "status-icon"), "success"))
wait.until(element_count_is((By.CSS_SELECTOR, ".search-result"), 10))
Error Handling
Catching TimeoutException
from selenium.common.exceptions import TimeoutException
try:
element = wait.until(EC.visibility_of_element_located((By.ID, "result")))
process(element)
except TimeoutException:
# Element didn't appear in time
screenshot = driver.get_screenshot_as_png()
save_debug_screenshot(screenshot)
print(f"Timed out. Current URL: {driver.current_url}")
print(f"Page title: {driver.title}")
raise
Adding Descriptive Messages
WebDriverWait's .until() accepts a message parameter for clearer TimeoutException output:
element = wait.until(
EC.element_to_be_clickable((By.ID, "checkout-button")),
message="Checkout button was not clickable after 10 seconds. Check if cart is empty."
)
When this times out, the exception will include your message — far more helpful than a generic timeout.
Common Mistakes
Mistake 1: Reusing WebDriverWait After TimeoutException
# ❌ After TimeoutException, the wait's internal timer state is unclear
try:
wait.until(EC.visibility_of_element_located((By.ID, "slow-element")))
except TimeoutException:
pass
# This may not give a full fresh 10-second window
wait.until(EC.visibility_of_element_located((By.ID, "another-element")))
# ✅ Create a new WebDriverWait when you need a fresh timeout
try:
wait.until(EC.visibility_of_element_located((By.ID, "slow-element")))
except TimeoutException:
pass
fresh_wait = WebDriverWait(driver, 10)
fresh_wait.until(EC.visibility_of_element_located((By.ID, "another-element")))
Mistake 2: Using presence When You Need visibility
# ❌ Element is in DOM but hidden — clicking fails
element = wait.until(EC.presence_of_element_located((By.ID, "modal-button")))
element.click() # ElementNotInteractableException
# ✅ Wait for visibility before clicking
element = wait.until(EC.visibility_of_element_located((By.ID, "modal-button")))
element.click()
# ✅ Or use element_to_be_clickable (visibility + enabled)
element = wait.until(EC.element_to_be_clickable((By.ID, "modal-button")))
element.click()
Mistake 3: Setting timeout Too Short
# ❌ 1 second is too short for most real-world operations
wait = WebDriverWait(driver, 1)
wait.until(EC.url_contains("/dashboard")) # Often times out
# ✅ Use realistic timeouts
wait = WebDriverWait(driver, 10) # 10s standard, 30s for slow operations
Mistake 4: Mixing with Implicit Wait
# ❌ Undefined behavior
driver.implicitly_wait(10)
wait = WebDriverWait(driver, 10)
element = wait.until(EC.visibility_of_element_located((By.ID, "element")))
# ✅ Explicit waits only
driver.implicitly_wait(0)
wait = WebDriverWait(driver, 10)
element = wait.until(EC.visibility_of_element_located((By.ID, "element")))
WebDriverWait in pytest Fixtures
The standard pattern for pytest-based Selenium tests:
import pytest
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
@pytest.fixture(scope="function")
def driver():
driver = webdriver.Chrome()
driver.implicitly_wait(0) # Always disable implicit wait
driver.maximize_window()
yield driver
driver.quit()
@pytest.fixture(scope="function")
def wait(driver):
return WebDriverWait(driver, 10)
@pytest.fixture(scope="function")
def slow_wait(driver):
return WebDriverWait(driver, 30)
# Usage in tests
def test_checkout_flow(driver, wait, slow_wait):
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
driver.get("https://shop.example.com/cart")
checkout = wait.until(EC.element_to_be_clickable((By.ID, "checkout-btn")))
checkout.click()
# Slow operation — payment processing
slow_wait.until(EC.url_contains("/confirmation"))
The Alternative: Zero Wait Configuration
Every Selenium test needs a WebDriverWait strategy. HelpMeTest removes this requirement entirely:
Go to https://shop.example.com/cart
Click the "Checkout" button
Fill in the shipping address form
Click "Place Order"
Verify the order confirmation number appears
No WebDriverWait(driver, 10). No expected_conditions. No TimeoutException debugging. The AI automatically waits for elements to be ready before interacting, exactly like a human.
WebDriverWait is essential when:
- Maintaining existing Selenium test suites
- Tests require precise timing control
- Teams are deeply invested in Selenium infrastructure
- You need cross-browser testing at the protocol level
When to reconsider the whole approach:
- New projects with no legacy Selenium investment
- Tests break frequently on timing-related failures
- Non-technical stakeholders need to understand or write tests
Summary
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
# Setup (in fixture)
driver.implicitly_wait(0)
wait = WebDriverWait(driver, 10)
# Most common patterns
element = wait.until(EC.visibility_of_element_located((By.ID, "id")))
element = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, ".btn")))
element = wait.until(EC.presence_of_element_located((By.NAME, "field")))
wait.until(EC.url_contains("/expected-path"))
wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, ".spinner")))
# Custom condition
wait.until(lambda d: len(d.find_elements(By.CSS_SELECTOR, ".item")) > 5)