Selenium WebDriverWait: Complete Python Guide with Examples (2026)

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
# 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)

Read more