Selenium tests often fail when a page updates after WebDriver has already looked for an element. An explicit wait keeps the test on the browser state it needs, such as visible text or a clickable button, instead of guessing with a fixed sleep.
In Python, WebDriverWait polls the browser until an expected condition returns a matching value or Selenium raises TimeoutException. The returned value is often the fresh WebElement that matched the condition, so the next action can use the same element without a second lookup.
Match each wait to the next action. Use visibility_of_element_located() before reading text, element_to_be_clickable() before a click, and a text condition when a status message proves completion. A high implicit wait can make explicit-wait polling harder to reason about, so keep implicit waits disabled unless a project standard requires otherwise.
Related: How to click a button with Selenium
Related: How to run a headless browser in Selenium
from pathlib import Path from tempfile import TemporaryDirectory import shutil from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait HTML = """<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>Selenium explicit wait demo</title> <script> window.addEventListener("DOMContentLoaded", () => { const status = document.querySelector("#status"); const button = document.querySelector("#submit-order"); setTimeout(() => { status.textContent = "Ready to submit"; button.disabled = false; }, 350); button.addEventListener("click", () => { status.textContent = "Submitted"; }); }); </script> </head> <body> <p id="status">Loading form</p> <button id="submit-order" disabled>Submit order</button> </body> </html> """ options = Options() options.add_argument("--headless=new") options.add_argument("--window-size=1280,720") for browser_name in ("google-chrome", "chromium", "chromium-browser"): browser_path = shutil.which(browser_name) if browser_path: options.binary_location = browser_path break driver_path = shutil.which("chromedriver") service = Service(driver_path) if driver_path else Service() with TemporaryDirectory() as tmpdir: page = Path(tmpdir) / "explicit-wait-demo.html" page.write_text(HTML, encoding="utf-8") driver = webdriver.Chrome(service=service, options=options) try: driver.get(page.as_uri()) wait = WebDriverWait(driver, 10, poll_frequency=0.2) wait.until( EC.text_to_be_present_in_element((By.ID, "status"), "Ready to submit"), "status text did not show the form was ready", ) button = wait.until( EC.element_to_be_clickable((By.ID, "submit-order")), "submit button did not become clickable", ) button.click() wait.until( EC.text_to_be_present_in_element((By.ID, "status"), "Submitted"), "status text did not show submission", ) print(f"title: {driver.title}") print(f"button_text: {button.text}") print(f"status: {driver.find_element(By.ID, 'status').text}") finally: driver.quit()
The page starts with disabled controls, changes the status text after JavaScript runs, then enables the button. The waits prove each state before the script moves on.
$ python3 selenium-explicit-waits-use.py title: Selenium explicit wait demo button_text: Submit order status: Submitted
wait = WebDriverWait(driver, 10, poll_frequency=0.5)
The timeout is the upper bound, not a fixed delay. WebDriverWait continues as soon as the condition returns a matching value.
submit_button = wait.until( EC.element_to_be_clickable((By.CSS_SELECTOR, "button[data-testid='submit-order']")), "submit button did not become clickable", ) submit_button.click()
element_to_be_clickable() checks that the element is visible and enabled. Use visibility_of_element_located() when the next action only needs visible text.
wait.until( EC.text_to_be_present_in_element( (By.CSS_SELECTOR, "[data-testid='order-status']"), "Submitted", ), "order status did not change to Submitted", )
A successful click() call only proves the browser received the click command. The follow-up condition proves the application reached the expected state.
wait.until( EC.invisibility_of_element_located((By.CSS_SELECTOR, "[data-testid='loading']")), "loading overlay did not disappear", )
invisibility_of_element_located() returns true when the element is hidden or no longer present, which fits temporary spinners and overlays.
driver.implicitly_wait(0)
Implicit waits change element lookup polling for the whole driver session. Keeping them off makes each explicit wait's timeout and locator message easier to interpret.
Related: How to troubleshoot Selenium timeout errors
$ rm selenium-explicit-waits-use.py