Standard expected conditions cover most scenarios, but real-world applications sometimes need custom synchronisation logic. A dropdown that populates after an AJAX call, a table that finishes sorting, an animation that must complete before interaction — these situations require custom expected conditions. Fluent waits add additional control over polling intervals and exception handling, giving you fine-grained control over the wait behaviour.
Custom Expected Conditions and Fluent Wait Configuration
A custom expected condition is simply a callable that returns a truthy value when the condition is met, or a falsy value (or raises an exception) when it is not. WebDriverWait calls your callable repeatedly until it succeeds or the timeout expires.
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 StaleElementReferenceException, NoSuchElementException
# ── Custom Expected Condition: element has specific attribute value ──
class element_attribute_to_be:
def __init__(self, locator, attribute, value):
self.locator = locator
self.attribute = attribute
self.value = value
def __call__(self, driver):
try:
element = driver.find_element(*self.locator)
current = element.get_attribute(self.attribute)
if current == self.value:
return element
return False
except (NoSuchElementException, StaleElementReferenceException):
return False
# ── Custom Expected Condition: table has finished loading rows ──
class table_row_count_at_least:
def __init__(self, table_locator, row_selector, min_count):
self.table_locator = table_locator
self.row_selector = row_selector
self.min_count = min_count
def __call__(self, driver):
try:
table = driver.find_element(*self.table_locator)
rows = table.find_elements(By.CSS_SELECTOR, self.row_selector)
if len(rows) >= self.min_count:
return rows
return False
except (NoSuchElementException, StaleElementReferenceException):
return False
# ── Custom Expected Condition using a lambda ──
# Wait for the page to have no active AJAX requests (jQuery)
def no_active_ajax(driver):
return driver.execute_script("return jQuery.active === 0")
# ── Usage examples ──
# Wait for a dropdown to have the value "completed"
# WebDriverWait(driver, 10).until(
# element_attribute_to_be(
# (By.ID, "order-status"), "data-status", "completed"
# )
# )
# Wait for a table to have at least 5 rows
# rows = WebDriverWait(driver, 15).until(
# table_row_count_at_least(
# (By.ID, "results-table"), "tbody tr", 5
# )
# )
# Wait for jQuery AJAX to finish
# WebDriverWait(driver, 10).until(no_active_ajax)
# ── Fluent Wait — custom polling and ignored exceptions ──
# Python's WebDriverWait already supports these via constructor parameters:
# Fluent wait with 0.25s polling and ignoring stale element exceptions
# wait = WebDriverWait(
# driver,
# timeout=15,
# poll_frequency=0.25, # Check every 250ms (default is 500ms)
# ignored_exceptions=[
# StaleElementReferenceException, # Ignore stale during polling
# NoSuchElementException, # Ignore not-found during polling
# ]
# )
# element = wait.until(EC.element_to_be_clickable((By.ID, "dynamic-btn")))
print("Custom Expected Conditions")
print("=" * 55)
print(" element_attribute_to_be — wait for attribute to have specific value")
print(" table_row_count_at_least — wait for table to populate with N rows")
print(" no_active_ajax — wait for jQuery AJAX to complete")
print()
print("Fluent Wait Parameters:")
print(" timeout — maximum wait duration")
print(" poll_frequency — how often to check (default 500ms)")
print(" ignored_exceptions — exceptions to suppress during polling")
True. Returning the element itself (as in element_attribute_to_be) is a best practice because it lets you chain: element = wait.until(my_condition); element.click(). Returning True means you have to find the element again after the wait completes. Returning the element directly saves a redundant lookup and avoids a potential race condition between the wait and the subsequent interaction.jQuery.active === 0 check is a powerful way to wait for all AJAX requests to complete. For modern apps using the Fetch API instead of jQuery, use: driver.execute_script("return window.performance.getEntriesByType('resource').filter(r => !r.responseEnd).length === 0"). These JavaScript-level waits are more reliable than waiting for specific DOM changes because they target the root cause (network requests) rather than the symptom (element updates).poll_frequency too low (e.g., 10ms) creates excessive CPU usage as Selenium hammers the browser with requests. The default 500ms is appropriate for most scenarios. Lower it to 100-250ms only for time-critical interactions where the 500ms granularity causes noticeable test slowness. Never set it below 50ms — the overhead of each poll request makes sub-50ms polling counterproductive.Common Mistakes
Mistake 1 — Writing a custom condition when a built-in one exists
❌ Wrong: Writing a custom callable to check if an element is visible when EC.visibility_of_element_located already does exactly that.
✅ Correct: Checking the built-in expected conditions list first. Custom conditions are only needed when no built-in condition matches your scenario — attribute value checks, row count checks, AJAX completion, and other application-specific states.
Mistake 2 — Not handling StaleElementReferenceException in custom conditions
❌ Wrong: A custom condition finds an element and reads its text, but does not catch StaleElementReferenceException. If the DOM re-renders between the find and the read, the condition crashes instead of retrying.
✅ Correct: Wrapping element interactions in try/except (StaleElementReferenceException, NoSuchElementException): return False. This tells the wait to retry on the next poll instead of aborting.