---
title: 'Selenium waits in Python: WebDriverWait and time.sleep'
description: Most flaky Selenium tests are timing bugs. The test looks for an element
  a few hundred milliseconds before the page&#39;s JavaScript has created it, and…
source_url:
  html: https://testingbot.com/resources/courses/selenium-python/waits
  md: https://testingbot.com/resources/courses/selenium-python/waits.md
---

Course

[Selenium with Python and pytest](https://testingbot.com/resources/courses/selenium-python)

Chapter 4 of 10

1. [1 Selenium Python Tutorial with pytest](https://testingbot.com/resources/courses/selenium-python)
2. [2 Set up Selenium with Python and pytest](https://testingbot.com/resources/courses/selenium-python/setup-and-first-test)
3. [3 Selenium locators in Python: the By API](https://testingbot.com/resources/courses/selenium-python/locators)
4. [4 Selenium waits in Python: WebDriverWait and time.sleep](https://testingbot.com/resources/courses/selenium-python/waits)
5. [5 pytest fixtures for Selenium: conftest.py and scope](https://testingbot.com/resources/courses/selenium-python/pytest-fixtures)
6. [6 Assertions, screenshots on failure and test reports](https://testingbot.com/resources/courses/selenium-python/assertions-screenshots-reports)
7. [7 Page objects in Python](https://testingbot.com/resources/courses/selenium-python/page-objects)
8. [8 Run Selenium tests in parallel with pytest-xdist](https://testingbot.com/resources/courses/selenium-python/parallel-with-pytest-xdist)
9. [9 Run Selenium Python tests on the TestingBot grid](https://testingbot.com/resources/courses/selenium-python/run-on-testingbot)
10. [10 Selenium Python Quiz](https://testingbot.com/resources/courses/selenium-python/knowledge-check)

[Exit course](https://testingbot.com/resources/courses)

 Chapter 4 of 10 Intermediate 6 min read 2 quick checks Updated Nov 2026 

# Selenium waits in Python: WebDriverWait and time.sleep
On this page
1. [The failure](https://testingbot.com#the-failure)
2. [Wrong fix 1: time.sleep](https://testingbot.com#wrong-fix-1-time-sleep)
3. [Wrong fix 2: a large implicit wait](https://testingbot.com#wrong-fix-2-a-large-implicit-wait)
4. [The right fix: WebDriverWait](https://testingbot.com#the-right-fix-webdriverwait)
5. [Choosing the condition](https://testingbot.com#choosing-the-condition)
6. [Your own conditions](https://testingbot.com#your-own-conditions)
7. [Three rules](https://testingbot.com#three-rules)

Most flaky Selenium tests are timing bugs. The test looks for an element a few hundred milliseconds before the page's JavaScript has created it, and fails on a slow CI machine but not on your laptop. This chapter shows the failure, the two wrong fixes, and the right one.

## The failure[#](https://testingbot.com#the-failure)

The practice page `dynamic.html` has an "Add a box!" button. Clicking it runs `setTimeout` and appends a red box, `#box0`, one second later. Create `tests/test_waits.py`:

```python
import time

import pytest
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

DYNAMIC = "https://www.selenium.dev/selenium/web/dynamic.html"

@pytest.fixture
def driver():
    driver = webdriver.Chrome()
    yield driver
    driver.quit()

def test_without_a_wait(driver):
    driver.get(DYNAMIC)
    driver.find_element(By.ID, "adder").click()
    driver.find_element(By.ID, "box0") # NoSuchElementException
```

Run it and pytest reports:

```
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":"[id="box0"]"}
```

The locator is correct. The element simply does not exist yet: `find_element` looks once, immediately, and gives up.

## Wrong fix 1: time.sleep[#](https://testingbot.com#wrong-fix-1-time-sleep)

```python
def test_with_sleep(driver):
    driver.get(DYNAMIC)
    driver.find_element(By.ID, "adder").click()
    time.sleep(2) # passes, but always costs 2 seconds and still guesses
    assert driver.find_element(By.ID, "box0").is_displayed()
```

This passes. It also costs two full seconds every run, even when the box appears after one, and two seconds is a guess: on a loaded CI agent or a slower remote browser the delay may be longer, and the test fails again. Multiply by a few hundred tests and a suite full of sleeps is both slow _and_ flaky, the worst combination.

## Wrong fix 2: a large implicit wait[#](https://testingbot.com#wrong-fix-2-a-large-implicit-wait)

```python
driver.implicitly_wait(10)
```

An implicit wait makes every `find_element` call keep retrying for up to that many seconds before raising. It looks convenient, but it is a global setting with side effects:

- It only waits for an element to be **present** in the DOM, not visible, enabled or holding the right text.
- `find_elements` on something that is correctly absent now takes the full ten seconds to return an empty list.
- It interacts badly with explicit waits. The Selenium documentation warns not to mix them: a 10-second implicit wait combined with a 15-second explicit wait can time out after 20 seconds.

Leave the implicit wait at its default of 0 and use explicit waits everywhere.

## The right fix: WebDriverWait[#](https://testingbot.com#the-right-fix-webdriverwait)

An explicit wait polls a condition until it is satisfied or a timeout expires:

```python
def test_with_explicit_wait(driver):
    driver.get(DYNAMIC)
    driver.find_element(By.ID, "adder").click()
    wait = WebDriverWait(driver, timeout=5)
    box = wait.until(EC.presence_of_element_located((By.ID, "box0")))
    assert box.get_dom_attribute("class") == "redbox"
```

`wait.until(...)` calls the condition every 0.5 seconds (the default poll interval) and returns the first truthy value it produces, here the element itself. If five seconds pass without success it raises `TimeoutException`. When we timed the two versions with `pytest --durations=5`, the sleep version took 2.24 seconds and the explicit wait 1.15: it continues as soon as the box exists.

Note the double parentheses. Expected conditions take a **locator tuple** , `(By.ID, "box0")`, not two arguments. `EC.presence_of_element_located(By.ID, "box0")` fails with a `TypeError`.

A test fails on CI about one run in ten with `NoSuchElementException` on a results table that loads via `fetch()`. What is the best fix?

 Add `time.sleep(5)` before reading the table. Wait with `WebDriverWait` for the condition the next line needs, such as the first row being visible. Set `driver.implicitly_wait(30)` in the fixture.

The explicit wait continues as soon as the row is there and fails with a clear timeout if it never comes. A sleep is still a guess that slows every run, and a large implicit wait only checks presence and slows down every absent-element check in the suite.

## Choosing the condition[#](https://testingbot.com#choosing-the-condition)

The `expected_conditions` module (imported as `EC` by convention) has ready-made conditions. Wait for the thing the **next line** depends on:

| Condition | Satisfied when | Use before |
| --- | --- | --- |
| `presence_of_element_located(loc)` | the element is in the DOM, maybe hidden | reading an attribute |
| `visibility_of_element_located(loc)` | in the DOM and displayed | reading text, typing |
| `visibility_of(element)` | an element you already have becomes displayed | typing into a revealed field |
| `element_to_be_clickable(loc)` | visible and enabled | clicking |
| `text_to_be_present_in_element(loc, text)` | the element's text contains `text` | asserting on updated text |
| `invisibility_of_element_located(loc)` | gone or hidden | continuing after a spinner |
| `url_contains(fragment)` | the URL contains `fragment` | asserting after a redirect |
| `number_of_windows_to_be(n)` | `n` windows or tabs are open | switching to a new tab |

The same page has a hidden input that a second button reveals after a second. Waiting for presence would return immediately (the input is always in the DOM), and typing into it would fail with `ElementNotInteractableException`. Visibility is the right condition:

```python
revealed = driver.find_element(By.ID, "revealed")
driver.find_element(By.ID, "reveal").click()
WebDriverWait(driver, 10).until(EC.visibility_of(revealed))
revealed.send_keys("now visible")
```

## Your own conditions[#](https://testingbot.com#your-own-conditions)

A condition is any callable that takes the driver and returns something truthy when done. Falsy means "keep polling":

```python
def test_custom_condition(driver):
    driver.get(DYNAMIC)
    adder = driver.find_element(By.ID, "adder")
    for _ in range(3):
        adder.click()

    def three_boxes(d):
        boxes = d.find_elements(By.CLASS_NAME, "redbox")
        return boxes if len(boxes) == 3 else False # falsy means "keep polling"

    boxes = WebDriverWait(driver, timeout=5).until(three_boxes)
    assert len(boxes) == 3
```

Give `until` a `message` and a timeout tells you what was missing instead of an empty `TimeoutException`:

```python
def test_timeout_message(driver):
    driver.get(DYNAMIC)
    wait = WebDriverWait(driver, timeout=2)
    with pytest.raises(TimeoutException):
        wait.until(EC.visibility_of_element_located((By.ID, "revealed")),
                   message="#revealed never became visible")
```

`WebDriverWait` also accepts `poll_frequency` and `ignored_exceptions`, for example `ignored_exceptions=[StaleElementReferenceException]` when the page re-renders the element you are polling.

A field is always in the HTML but hidden until a checkbox is ticked. You wait with `presence_of_element_located`, then call `send_keys`, and get `ElementNotInteractableException`. Why?

 The locator is wrong. The timeout is too short. Presence is true from the start, so the wait returned at once while the field was still hidden; wait for visibility instead.

The element exists in the DOM all along, so a presence check is satisfied immediately. Raising the timeout changes nothing because the condition never waits at all. `visibility_of_element_located` or `element_to_be_clickable` describes what typing actually needs.

## Three rules[#](https://testingbot.com#three-rules)

1. **Wait for a condition, never for a duration.** `time.sleep` belongs in exactly zero tests.
2. **Keep the implicit wait at 0** so explicit waits behave predictably.
3. **Do not "fix" an intermittent failure by raising the timeout.** Find which condition was not yet true, and wait for that. Our article on [handling test flakiness](https://testingbot.com/resources/articles/handling-test-flakiness) covers the other common causes.

Every test file so far has started with its own copy of the `driver` fixture. The next chapter moves it into `conftest.py` and makes it configurable.

[Previous · Chapter 3 Selenium locators in Python: the By API](https://testingbot.com/resources/courses/selenium-python/locators) [Next · Chapter 5 pytest fixtures for Selenium: conftest.py and scope](https://testingbot.com/resources/courses/selenium-python/pytest-fixtures)

On this page

1. [The failure](https://testingbot.com#the-failure)
2. [Wrong fix 1: time.sleep](https://testingbot.com#wrong-fix-1-time-sleep)
3. [Wrong fix 2: a large implicit wait](https://testingbot.com#wrong-fix-2-a-large-implicit-wait)
4. [The right fix: WebDriverWait](https://testingbot.com#the-right-fix-webdriverwait)
5. [Choosing the condition](https://testingbot.com#choosing-the-condition)
6. [Your own conditions](https://testingbot.com#your-own-conditions)
7. [Three rules](https://testingbot.com#three-rules)

Certification

This course has a free, hands-on certificate.

[How it works](https://testingbot.com/resources/courses/selenium-python/certification)
