Explicit Waits with WebDriverWait in Python

6 min read

Mobile apps are asynchronous. Network calls return late, animations complete over time, view transitions don't happen instantly. Explicit waits — polling until a condition is true — beat time.sleep() on both speed and reliability.

Why not time.sleep()

time.sleep(3) always waits 3 seconds, even when the element appears in 300ms. Across 150 tests, that's 7.5 minutes of guaranteed waste. Explicit waits return as soon as the condition is met.

WebDriverWait basics

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
 
wait = WebDriverWait(driver, 15)  # 15-second timeout
 
# Wait for visibility
element = wait.until(EC.visibility_of_element_located(
    (AppiumBy.ACCESSIBILITY_ID, "submitButton")
))
 
# Wait for clickable (visible + enabled)
button = wait.until(EC.element_to_be_clickable(
    (AppiumBy.ACCESSIBILITY_ID, "loginButton")
))
 
# Wait for text
wait.until(EC.text_to_be_present_in_element(
    (AppiumBy.ACCESSIBILITY_ID, "statusLabel"),
    "Order Placed"
))
 
# Wait for invisibility (loading spinners)
wait.until(EC.invisibility_of_element_located(
    (AppiumBy.ACCESSIBILITY_ID, "loadingSpinner")
))

Common expected conditions

ConditionUse case
visibility_of_element_locatedElement exists and has non-zero size
element_to_be_clickableVisible + enabled (handles grayed-out buttons)
invisibility_of_element_locatedWait for spinner to disappear
presence_of_element_locatedIn DOM but may be invisible
text_to_be_present_in_elementValue updates after async load
number_of_elements_to_be_more_thanList has populated

Custom wait conditions

Lambda-based conditions for cases expected_conditions doesn't cover:

# Wait for element count to stabilise
def wait_for_list_to_load(driver, locator, min_count=1, timeout=15):
    return WebDriverWait(driver, timeout).until(
        lambda d: len(d.find_elements(*locator)) >= min_count
    )
 
items = wait_for_list_to_load(driver, (AppiumBy.ACCESSIBILITY_ID, "listItem"))
 
# Wait for attribute value
def wait_for_checked(driver, locator, timeout=10):
    return WebDriverWait(driver, timeout).until(
        lambda d: d.find_element(*locator).get_attribute("checked") == "true"
    )
 
# Wait for specific activity (Android)
def wait_for_activity(driver, expected_activity, timeout=10):
    WebDriverWait(driver, timeout).until(
        lambda d: expected_activity in d.current_activity
    )

Return None or False to keep polling; return any other value to stop and return it.

FluentWait equivalent in Python

Python's WebDriverWait accepts poll_frequency and ignored_exceptions:

from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException
 
wait = WebDriverWait(
    driver,
    timeout=20,
    poll_frequency=0.5,            # check every 500ms
    ignored_exceptions=[
        NoSuchElementException,
        StaleElementReferenceException,
    ]
)
 
element = wait.until(
    EC.visibility_of_element_located((AppiumBy.ACCESSIBILITY_ID, "result"))
)

ignored_exceptions prevents polling from stopping when the element hasn't appeared yet but find_element throws. Without it, a NoSuchElementException during polling terminates the wait immediately.

Waiting for animations

After a tap triggers an animation, wait for the element to reach its stable size:

import time
 
def wait_for_stable_element(driver, locator, timeout=5):
    """Wait for element dimensions to stop changing (animation complete)."""
    end_time = time.time() + timeout
    last_height = None
 
    while time.time() < end_time:
        try:
            el = driver.find_element(*locator)
            current_height = el.size["height"]
            if current_height == last_height and last_height is not None:
                return el
            last_height = current_height
        except Exception:
            pass
        time.sleep(0.2)
 
    raise TimeoutException(f"Element at {locator} did not stabilise within {timeout}s")

Loading spinner pattern

After triggering an async operation, wait for the spinner to appear and then disappear:

SPINNER = (AppiumBy.ACCESSIBILITY_ID, "loadingIndicator")
 
def wait_for_load_complete(driver, trigger_spinner_timeout=2, load_timeout=30):
    """
    First waits for the spinner to appear (up to trigger_spinner_timeout),
    then waits for it to disappear (up to load_timeout).
    """
    try:
        # Wait for spinner to appear
        WebDriverWait(driver, trigger_spinner_timeout).until(
            EC.visibility_of_element_located(SPINNER)
        )
        # Then wait for it to go away
        WebDriverWait(driver, load_timeout).until(
            EC.invisibility_of_element_located(SPINNER)
        )
    except TimeoutException:
        # Spinner appeared and disappeared faster than we checked — OK
        pass

Centralised wait utils

Put common patterns in utils/wait_utils.py:

# utils/wait_utils.py
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
 
 
class WaitUtils:
    def __init__(self, driver, default_timeout=15, short_timeout=5, long_timeout=30):
        self.driver = driver
        self.default = default_timeout
        self.short = short_timeout
        self.long = long_timeout
 
    def for_visible(self, locator, timeout=None):
        t = timeout or self.default
        return WebDriverWait(self.driver, t).until(
            EC.visibility_of_element_located(locator)
        )
 
    def for_clickable(self, locator, timeout=None):
        t = timeout or self.default
        return WebDriverWait(self.driver, t).until(
            EC.element_to_be_clickable(locator)
        )
 
    def for_invisible(self, locator, timeout=None):
        t = timeout or self.long
        WebDriverWait(self.driver, t).until(
            EC.invisibility_of_element_located(locator)
        )
 
    def is_present(self, locator, timeout=None):
        t = timeout or self.short
        try:
            WebDriverWait(self.driver, t).until(
                EC.presence_of_element_located(locator)
            )
            return True
        except TimeoutException:
            return False

// tip to track lessons you complete and pick up where you left off across devices.