Chapter 06Lesson 02~205 minutes

Synchronization: Implicit, Explicit, Fluent, and Custom Waits: Guided Hands-On Workflow

Operate a disposable delayed-render fixture and compare immediate lookup, an intentionally bad fixed sleep, isolated implicit waiting, explicit Expected Conditions, and a custom domain condition with timing evidence.

Loopback labExpected ConditionsCustom waitTiming evidenceNo mixed waits

Learning objectives

  • Create a disposable loopback delayed-render fixture with deterministic timing and synthetic state.
  • Measure immediate lookup failure and an intentionally brittle fixed-sleep baseline without treating either as the solution.
  • Demonstrate implicit waiting in its own session and keep explicit/custom-wait sessions at an implicit wait of zero.
  • Use Expected Conditions for presence/visibility and a custom condition for domain-specific application readiness.
  • Record elapsed time, session identity, browser capabilities, state snapshots, exceptions, and screenshots so causality is visible.
  • Choose the correct wait condition for a new scenario instead of copying a timeout value.

1. Lab contract and timing map

This lab uses a static fixture served only from 127.0.0.1. The page accepts a deterministic ?delay= query value, renders a result after that delay, then changes the application status to ready. Nothing calls an external API, uses a real account, or stores credentials. The purpose is to make the race reproducible enough to measure.

Important: the fixed-sleep example below is intentionally bad and exists only for comparison. It is not the final synchronization strategy. The explicit/custom-wait sessions keep their implicit wait at zero.

The following table organizes the key choices and evidence for Lab contract and timing map. Use it together with the surrounding prose so the rows serve as a comparison aid rather than standalone rules.

State Before trigger During delay Ready
#status[data-state] idle loading ready
#result absent absent present + visible
Session alive alive alive
Evidence baseline metadata elapsed measurement result + screenshot/timing record

2. Create the disposable project

The following example makes the Create the disposable project behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

mkdir selenium-waits-lab
cd selenium-waits-lab
python -m venv .venv
# Linux/macOS:
. .venv/bin/activate
# Windows PowerShell:
# .\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install "selenium==4.47.0"
mkdir site evidence

The following example makes the Create the disposable project behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

python -c "import selenium; print('selenium', selenium.__version__)"
python --version

Start a supported local browser at least once if the environment has never resolved its driver. Modern Selenium normally invokes Selenium Manager automatically when no driver path is supplied.

3. Create the deterministic delayed-render fixture

The following example makes the Create the deterministic delayed-render fixture behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Synchronization Fixture</title></head>
<body data-build="ch06-l2-v1">
  <h1>Delayed result</h1>
  <button id="start">Start</button>
  <div id="status" data-state="idle">Idle</div>
  <div id="mount"></div>
<script>
const params = new URLSearchParams(location.search);
const delay = Number(params.get('delay') || '1200');
const status = document.querySelector('#status');
const mount = document.querySelector('#mount');
document.querySelector('#start').addEventListener('click', () => {
  status.dataset.state = 'loading';
  status.textContent = `Loading for ${delay} ms`;
  mount.replaceChildren();
  setTimeout(() => {
    const result = document.createElement('p');
    result.id = 'result';
    result.dataset.requestId = `REQ-${delay}`;
    result.textContent = `Result after ${delay} ms`;
    mount.append(result);
    status.dataset.state = 'ready';
    status.textContent = 'Ready';
  }, delay);
});
</script></body></html>

Save as site/index.html, then run this in a separate terminal:

python -m http.server 8765 --bind 127.0.0.1 --directory site

4. Baseline A: immediate lookup fails for the right reason

The following example makes the Baseline A: immediate lookup fails for the right reason behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

from time import monotonic
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException

URL = "http://127.0.0.1:8765/?delay=1200"
driver = webdriver.Chrome()
try:
    driver.get(URL)
    assert driver.timeouts.implicit_wait == 0
    driver.find_element(By.ID, "start").click()
    t0 = monotonic()
    try:
        driver.find_element(By.ID, "result")
    except NoSuchElementException as exc:
        print(type(exc).__name__, "after", round(monotonic() - t0, 3), "s")
        print("status", driver.find_element(By.ID, "status").get_dom_attribute("data-state"))
finally:
    driver.quit()

Expected observation: the lookup fails almost immediately while status is loading. That is not Selenium being unreliable; with an implicit wait of zero, the command accurately reports that the node does not exist yet.

5. Baseline B: a fixed sleep guesses instead of observing

The following example makes the Baseline B: a fixed sleep guesses instead of observing behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

# INTENTIONALLY BRITTLE DEMONSTRATION — do not use as the final solution.
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException

URL = "http://127.0.0.1:8765/?delay=1200"
driver = webdriver.Chrome()
try:
    driver.get(URL)
    driver.find_element(By.ID, "start").click()
    time.sleep(0.8)  # guessed delay: intentionally shorter than fixture transition
    try:
        print(driver.find_element(By.ID, "result").text)
    except NoSuchElementException:
        print("sleep expired but application is still", driver.find_element(By.ID, "status").text)
finally:
    driver.quit()

The failure is deterministic here because the fixture needs 1.2 seconds and the test guesses 0.8 seconds. Increasing the sleep to 2 seconds would hide the race for this one case while wasting time whenever the application finishes sooner.

6. Isolated implicit-wait demonstration

Use a fresh session so this global setting cannot contaminate the explicit-wait examples that follow.

from time import monotonic
from selenium import webdriver
from selenium.webdriver.common.by import By

URL = "http://127.0.0.1:8765/?delay=1200"
driver = webdriver.Chrome()
try:
    driver.implicitly_wait(2)
    driver.get(URL)
    driver.find_element(By.ID, "start").click()
    t0 = monotonic()
    result = driver.find_element(By.ID, "result")
    print("found after", round(monotonic() - t0, 3), "s")
    print(result.text)
finally:
    driver.quit()

The lookup returns soon after the node appears; it does not necessarily consume the full two seconds. This demonstrates the mechanism, but the global policy still cannot express “visible,” “ready,” or the expected request identity.

7. Explicit wait: bind the wait to the next required state

The following example makes the Explicit wait: bind the wait to the next required state behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

from time import monotonic
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

URL = "http://127.0.0.1:8765/?delay=1200"
driver = webdriver.Chrome()
try:
    assert driver.timeouts.implicit_wait == 0
    driver.get(URL)
    driver.find_element(By.ID, "start").click()
    wait = WebDriverWait(driver, 2.5)
    t0 = monotonic()
    result = wait.until(EC.visibility_of_element_located((By.ID, "result")))
    elapsed = monotonic() - t0
    assert result.text == "Result after 1200 ms"
    print("visible after", round(elapsed, 3), "s")
finally:
    driver.quit()

The timeout is a ceiling, not a sleep. The relevant contract is visibility of #result, because the next line reads what a user can see. If the condition never becomes true before 2.5 seconds, WebDriverWait raises TimeoutException; preserve the page state before changing the budget.

8. Custom condition: express domain readiness, not just DOM presence

The following example makes the Custom condition: express domain readiness, not just DOM presence behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

from time import monotonic
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait

class RequestReady:
    def __init__(self, request_id):
        self.request_id = request_id

    def __call__(self, driver):
        status = driver.find_element(By.ID, "status")
        if status.get_dom_attribute("data-state") != "ready":
            return False
        result = driver.find_element(By.ID, "result")
        return result if result.get_dom_attribute("data-request-id") == self.request_id else False

URL = "http://127.0.0.1:8765/?delay=1200"
driver = webdriver.Chrome()
try:
    assert driver.timeouts.implicit_wait == 0
    driver.get(URL)
    driver.find_element(By.ID, "start").click()
    t0 = monotonic()
    result = WebDriverWait(driver, 2.5, poll_frequency=0.2).until(RequestReady("REQ-1200"))
    print(result.text, "after", round(monotonic() - t0, 3), "s")
finally:
    driver.quit()

This wait proves two facts: the application phase is ready and the visible result belongs to the expected request. That is stronger than “some node named result exists.”

9. Record causality, not only pass/fail

The following example makes the Record causality, not only pass/fail behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

evidence = {
    "session_id": driver.session_id,
    "browser": driver.capabilities.get("browserName"),
    "browser_version": driver.capabilities.get("browserVersion"),
    "implicit_wait": driver.timeouts.implicit_wait,
    "url": driver.current_url,
    "status": driver.find_element(By.ID, "status").get_dom_attribute("data-state"),
    "request_id": result.get_dom_attribute("data-request-id"),
    "elapsed_seconds": round(elapsed, 3),
}
print(evidence)

On failure, capture the same state plus a screenshot and the first exception before teardown. The evidence should tell you whether the condition was never met, the wrong page loaded, the request identity changed, or the browser/session itself failed.

10. Challenge: choose the condition

A panel is inserted immediately, starts hidden, becomes visible after data arrives, and its Save button becomes enabled 300 ms later. Your next command must click Save. Which contract is strongest: presence of the panel, visibility of the panel, text in the panel, or clickability/enabled state of Save? Explain which state the next command actually requires and what additional application evidence you would assert after the click.

Knowledge check

Why is the fixed-sleep example included even though fixed sleeps are discouraged?

Why is the implicit-wait demonstration run in a separate session?

What does visibility_of_element_located add beyond presence?

Why does the custom RequestReady condition inspect request identity?

If a 2.5-second explicit wait succeeds after 1.2 seconds, how long should the next command wait?

Next lesson

Configuration, Design Patterns, and Trade-Offs

Turn these mechanisms into a repeatable wait policy for local development and CI instead of choosing timeouts ad hoc.

Official references and version notes

Version and compatibility note

Version-sensitive behavior was rechecked against Selenium primary documentation on 2026-08-28. Mandatory examples pin Selenium Python 4.47.0 on Python 3.10+, use a supported locally installed Chromium-family browser with ordinary Selenium Manager resolution, and target only loopback fixtures. Python has no separate Java-style FluentWait class: configurable fluent behavior is provided by WebDriverWait(timeout, poll_frequency, ignored_exceptions). Grid, browser clouds, enterprise identity, and BiDi are not required in this chapter. The intentionally brittle sleep is confined to a comparison baseline. The stable workflow uses bounded observable conditions and keeps explicit/custom sessions at an implicit wait of zero.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.