Python Selenium Waits: Implicit, Explicit, and WebDriverWait
python selenium waits explicit implicit and webdriverwait: Understand Selenium waits in Python: how implicit and explicit waits work, when to use WebDriverWait, and ho...
python selenium waits explicit implicit and webdriverwait requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When a Selenium test runs faster than the page can render, it fails with NoSuchElementException or ElementNotInteractableException. The fix is not to sprinkle time.sleep() calls throughout the test, but to use the waiting mechanisms Selenium provides. In Python, the three main tools are implicit waits, explicit waits, and the WebDriverWait class. Each solves a different problem, and choosing the right one has a direct impact on test reliability and runtime.
Why Waits Are Necessary in Selenium
Selenium interacts with the browser through the WebDriver protocol. After a page loads or an action triggers a request, the DOM may not immediately reflect the change. Elements may be present but not yet visible, clickable, or attached to the document. A wait tells WebDriver to poll the page until a condition is met or a timeout is reached. Without a wait, the test either fails immediately or, if you use time.sleep(), wastes seconds even when the element is ready earlier. The goal is to make the test wait only as long as necessary.
Implicit Wait: Setting a Global Timeout
The implicit wait is a single timeout applied to every element lookup for the lifetime of the WebDriver session. You set it once after creating the driver:
from selenium import webdriver driver = webdriver.Chrome() driver.implicitly_wait(10) # seconds
After this call, every find_element or find_elements will poll the DOM for up to 10 seconds before raising NoSuchElementException. The polling interval is implementation-defined, but typically around 500 milliseconds. The implicit wait is global: it applies to all element searches, regardless of the condition you actually need. If an element appears after 2 seconds, the search returns immediately; if it never appears, the full 10 seconds are consumed.
A common mistake is to set the implicit wait to a large value and then wonder why tests are slow. The implicit wait only delays the failure, not the success. If the element is found immediately, the wait has no effect. However, because it applies to every lookup, it can mask real problems. For example, if you expect an element to be missing and use find_elements to check its absence, the implicit wait will force a full timeout before returning an empty list, slowing down that assertion.
Explicit Wait with WebDriverWait and Expected Conditions
The explicit wait is a targeted, condition-based wait. You create a WebDriverWait instance, pass it a driver and a timeout, and then call until() with an expected condition. The condition is evaluated repeatedly until it returns a truthy value or the timeout expires.
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() wait = WebDriverWait(driver, 10) element = wait.until(EC.presence_of_element_located((By.ID, "submit")))
The expected_conditions module provides common conditions such as visibility_of_element_located, element_to_be_clickable, text_to_be_present_in_element, and staleness_of. Each condition is a callable that receives the driver and returns either True, an element, or False. When the condition returns a truthy value, until() returns that value, which is often the element itself. If the condition never becomes true, until() raises TimeoutException after the timeout.
Explicit waits are more precise than implicit waits because they wait for a specific state, not just for an element to exist. For example, an element may be present in the DOM but still be hidden or disabled. presence_of_element_located only checks that the element exists; element_to_be_clickable also checks that it is visible and enabled.
Comparing Implicit and Explicit Waits
The choice between implicit and explicit waits is not either/or. Many projects use both, but mixing them can lead to unexpected behavior if you are not careful. The table below summarizes the key differences.
| Aspect | Implicit Wait | Explicit Wait (WebDriverWait) |
|---|---|---|
| Scope | Global for all element lookups | Per condition, per element |
| Condition type | Element presence in DOM | Any condition (visibility, clickable, etc.) |
| Polling | Browser-dependent, usually ~500ms | Default 500ms, configurable |
| Timeout behavior | Applies to every find_element call | Applies only to the until() call |
| Error on timeout | NoSuchElementException | TimeoutException |
| Best used for | Simple, static pages | Dynamic pages with specific states |
A common practice is to set a short implicit wait (e.g., 2 seconds) as a safety net and use explicit waits for critical elements. However, this can cause problems because the implicit wait is still active during the explicit wait's polling. If an element is not found, the explicit wait may consume extra time because each find_element call inside the condition also waits for the implicit timeout. To avoid this, many teams choose one approach. If you use explicit waits extensively, consider setting the implicit wait to 0 to prevent double timeouts.
Writing Custom Expected Conditions
Sometimes the built-in expected conditions are not enough. For example, you might need to wait until an element has a specific CSS class, or until a value in a table changes. You can write a custom condition as a callable that takes the driver and returns a truthy value. Here is an example that waits for an element to contain a particular text:
class element_has_text: def __init__(self, locator, text): self.locator = locator self.text = text def __call__(self, driver): element = driver.find_element(*self.locator) return self.text in element.text wait.until(element_has_text((By.ID, "status"), "Success"))
The condition is instantiated with the locator and expected text. When called, it finds the element and checks its text. If the element is not found, find_element raises NoSuchElementException, which WebDriverWait catches and treats as a false condition. This pattern works because WebDriverWait ignores NoSuchElementException by default, but it does not ignore other exceptions. If your custom condition can raise other exceptions, you should handle them internally or use the ignored_exceptions parameter.
Using until_not and Configuring Polling
WebDriverWait also provides until_not(), which waits until a condition becomes false. This is useful for waiting for an element to disappear or for a loading spinner to be removed. For example:
wait.until_not(EC.presence_of_element_located((By.CLASS_NAME, "spinner")))
You can control the polling interval with the poll_frequency parameter. The default is 0.5 seconds, but you can reduce it for fast-changing pages or increase it to reduce load on the browser. You can also specify exceptions to ignore during polling. For instance, if an element is temporarily stale, you might ignore StaleElementReferenceException:
wait = WebDriverWait(driver, 10, poll_frequency=0.2, ignored_exceptions=[StaleElementReferenceException])
This configuration is useful when the DOM is being updated frequently and elements are recreated. Without ignoring the exception, the wait would abort immediately. By ignoring it, the condition is retried until the timeout.
Common Pitfalls and Maintainability Concerns
One of the most common pitfalls is mixing implicit and explicit waits without understanding the interaction. As mentioned, the implicit wait applies to every find_element call inside the explicit wait, which can lead to longer than expected timeouts. A second pitfall is using time.sleep() in place of a wait. This makes tests slower and brittle because the sleep duration is arbitrary. A third issue is using presence_of_element_located when you need the element to be visible or clickable. This leads to flaky tests that fail intermittently because the element is present but not yet ready.
From a maintainability perspective, centralizing wait logic helps. Instead of scattering WebDriverWait calls throughout your test code, you can create helper functions that encapsulate common conditions. For example, a click_after_visible function can wait for an element to be clickable and then click it. This reduces duplication and makes it easier to adjust timeouts globally.
Another operational concern is the cost of polling. Each poll sends a command to the browser, which consumes network and browser resources. Using a very short poll_frequency (e.g., 0.05 seconds) can significantly increase load on the browser and the machine running the tests. For most cases, the default 0.5 seconds is a reasonable balance between responsiveness and resource usage. If you need faster reaction, consider whether a shorter interval is truly necessary, or whether the underlying page can be made more deterministic.
Combining Waits for Complex Flows
In real applications, you often need to wait for a sequence of events. For example, after clicking a button, a modal appears, then a network request completes, and then a success message is shown. You can chain multiple until calls, but each call creates a new WebDriverWait instance. A cleaner approach is to reuse the same WebDriverWait instance, but be aware that the timeout is reset for each until call. If you want to wait for a total of 10 seconds across multiple conditions, you need to track the elapsed time yourself. Alternatively, you can write a single condition that checks all required states.
Here is an example that waits for both an element to be visible and a specific text to appear, using a custom condition:
class both_visible_and_text: def __init__(self, locator, text): self.locator = locator self.text = text def __call__(self, driver): element = driver.find_element(*self.locator) if not element.is_displayed(): return False return self.text in element.text wait.until(both_visible_and_text((By.ID, "message"), "Done"))
This condition combines visibility and text checking into one poll, reducing the number of round trips. For complex pages, this approach is more efficient than multiple until calls, because each poll only performs one find_element and a few property checks.
Finally, remember that waits are not a substitute for correct synchronization. If your application uses JavaScript to fetch data after a click, a wait for an element that is already present but not yet updated will pass prematurely. Always wait for the condition that actually indicates the operation is complete, such as a loading spinner disappearing or a new element appearing, rather than just the presence of a container that exists from the start.