ScreenshotNeo

BlogHow-to

How to Wait for a Page to Load with Python WebDriver

Learn when Selenium waits, how to choose page-load strategies, and how to synchronize Python WebDriver with dynamic pages reliably.

By the ScreenshotNeo team30 September 20268 min read

How to Wait for a Page to Load with Python WebDriver

Use driver.get() for document navigation, then use an explicit WebDriverWait for the exact state your next action needs. With Selenium’s default normal page-load strategy, get() waits for the document’s complete readiness state. That does not guarantee that a single-page app has finished its JavaScript requests or rendered the element you need.

The reliable Python pattern is:

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

driver = webdriver.Chrome()
try:
    driver.get("https://example.com")
    results = WebDriverWait(driver, 15).until(
        EC.visibility_of_element_located((By.CSS_SELECTOR, "[data-testid='results']"))
    )
    results.click()
finally:
    driver.quit()

Selenium documents this navigation/readiness distinction in its waiting guide. The Python API also exposes separate page-load and implicit timeouts in the WebDriver reference.

1. What “page loaded” means in Selenium

driver.get(url) blocks according to the session’s page-load strategy. Under normal (the default), WebDriver waits for the document’s readyState to reach complete, which corresponds to the load event. The HTML-defined resources may be ready at that point, while JavaScript can still add, replace, or hydrate elements afterward.

Navigation readiness and application readiness are separate checkpoints.
Navigation readiness and application readiness are separate checkpoints.
Need What to wait for Typical Selenium tool
Initial document navigation Browser document readiness driver.get() plus a page-load strategy
An element exists in the DOM Node is present, even if hidden presence_of_element_located
User can see an element Node is displayed visibility_of_element_located
A click is safe Displayed and enabled element_to_be_clickable
A route changed Expected URL or URL fragment url_contains or url_to_be
App-specific readiness Spinner gone, status ready, text present, or custom predicate Custom explicit wait

Choose the condition that makes the next operation safe. “The page loaded” is usually too broad for an AJAX or SPA workflow.

2. Install Selenium and create a driver

Install Selenium in the environment that will run the test or scraper:

python -m pip install -U selenium
from selenium import webdriver

driver = webdriver.Chrome()
try:
    driver.get("https://example.com")
    print(driver.title)
finally:
    driver.quit()

3. Use explicit waits for dynamic content

Presence, visibility, and clickability

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

driver = webdriver.Chrome()
wait = WebDriverWait(driver, 15)

try:
    driver.get("https://example.com/dashboard")
    results_node = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "[data-testid='results']")))
    results = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, "[data-testid='results']")))
    refresh = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "button.refresh")))
    refresh.click()
finally:
    driver.quit()

WebDriverWait polls the condition until it returns a truthy result or the timeout expires. A failed wait raises TimeoutException.

Wait for a title, URL, text, or an attribute

wait.until(EC.title_contains("Dashboard"))
wait.until(EC.url_contains("/reports"))
wait.until(EC.text_to_be_present_in_element((By.CSS_SELECTOR, "[role='status']"), "Ready"))
wait.until(EC.text_to_be_present_in_element_value((By.NAME, "q"), "selenium"))
wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, "[aria-busy='false']")))

Wait for a custom application condition

def results_have_rows(driver):
    rows = driver.find_elements(By.CSS_SELECTOR, "table tbody tr")
    return rows if rows else False

rows = WebDriverWait(driver, 20, poll_frequency=0.25).until(results_have_rows)
print(f"Rows: {len(rows)}")

4. Set a navigation timeout separately

driver.set_page_load_timeout(seconds) is a ceiling for navigation completion. It does not wait for a selector, an AJAX response, or a business-level “ready” state.

from selenium import webdriver
from selenium.common.exceptions import TimeoutException

driver = webdriver.Chrome()
driver.set_page_load_timeout(30)
try:
    try:
        driver.get("https://example.com/slow-page")
    except TimeoutException:
        print("Navigation exceeded 30 seconds")
finally:
    driver.quit()

See the Selenium Python API documentation.

5. Choose a page-load strategy

Strategy Returns when Use when Follow-up
normal complete and load event Conventional navigation behavior Still wait for dynamic UI state
eager interactive / DOMContentLoaded You synchronize application state yourself Wait for the next required element or state
none WebDriver does not block on readiness You have strong readiness signals Explicit waits are mandatory
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

options = webdriver.ChromeOptions()
options.page_load_strategy = "eager"
driver = webdriver.Chrome(options=options)
try:
    driver.get("https://example.com/app")
    WebDriverWait(driver, 20).until(
        EC.visibility_of_element_located((By.CSS_SELECTOR, "[data-testid='app-ready']"))
    )
finally:
    driver.quit()

The capability applies to the entire session. Click and form-submit navigation also needs condition-based waits.

6. Implicit waits

An implicit wait is a global delay applied to every element-location call. Its default is zero:

driver.implicitly_wait(5)

Do not mix implicit and explicit waits because their timers can interact unpredictably. For dynamic applications, keep implicit waits at zero and use explicit waits:

driver.implicitly_wait(0)
Approach Scope Failure Best fit
Page-load strategy Every navigation Navigation timeout Choosing when get() returns
Implicit wait Every element lookup NoSuchElementException Simple pages
Explicit wait One condition TimeoutException SPAs and AJAX
time.sleep() Fixed pause Too short or wasteful Rare deliberate pauses

7. Reusable wait helper

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

def open_until_visible(url, locator, navigation_seconds=30, condition_seconds=20):
    driver = webdriver.Chrome()
    driver.set_page_load_timeout(navigation_seconds)
    try:
        driver.get(url)
        element = WebDriverWait(driver, condition_seconds).until(
            EC.visibility_of_element_located(locator)
        )
        return driver, element
    except (TimeoutException, WebDriverException):
        driver.quit()
        raise

driver, results = open_until_visible(
    "https://example.com/dashboard",
    (By.CSS_SELECTOR, "[data-testid='results']")
)
try:
    print(results.text)
finally:
    driver.quit()

8. Common dynamic-page patterns

Wait for a loading indicator to disappear

wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, "[data-testid='loading']")))

Wait for a new page after a click

old_heading = driver.find_element(By.CSS_SELECTOR, "h1")
driver.find_element(By.CSS_SELECTOR, "a.next").click()
wait.until(EC.staleness_of(old_heading))
wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, "h1")))

Wait for a frame

wait.until(EC.frame_to_be_available_and_switch_to_it((By.CSS_SELECTOR, "iframe[data-app]")))
wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".inside-frame")))
driver.switch_to.default_content()

Wait for a new window

original = driver.current_window_handle
before = set(driver.window_handles)
driver.find_element(By.CSS_SELECTOR, "a[target='_blank']").click()
wait.until(lambda d: len(d.window_handles) > len(before))
new_handle = next(h for h in driver.window_handles if h != original)
driver.switch_to.window(new_handle)
wait.until(EC.presence_of_element_located((By.TAG_NAME, "body")))

Wait for an application flag

wait.until(lambda d: d.execute_script(
    "return document.querySelector('[data-app-ready]')?.dataset.appReady === 'true';"
))

document.readyState is useful for diagnostics, but does not prove that application data has finished loading.

9. Troubleshooting checklist

Symptom Cause Fix
get() returns but data is missing JavaScript ran after document readiness Wait for the result element, status text, spinner disappearance, or custom predicate.
TimeoutException Wrong locator, slow backend, wrong frame, or state never occurs Verify the selector and context; increase timeout only when justified.
NoSuchElementException Element is not present yet Use an explicit presence or visibility wait.
Click intercepted Overlay, banner, animation, or disabled control Wait for clickability and overlay disappearance.
StaleElementReferenceException Framework replaced the node Locate the element again inside the wait.
Element inside iframe is missing Wrong browsing context Switch into the frame before locating.
Navigation hangs Server or resource never completes Set page-load timeout; consider eager or none with explicit waits.
Mixed waits are slow Implicit and explicit waits compound Remove the implicit wait.
CI differs from local Different browser, viewport, network, or timing Capture browser/version, URL, screenshot, page source, and readiness state.
from pathlib import Path

Path("debug.png").write_bytes(driver.get_screenshot_as_png())
Path("debug.html").write_text(driver.page_source, encoding="utf-8")
print(driver.current_url, driver.title)
print(driver.execute_script("return document.readyState"))

10. Performance, reliability, and cost

  • Performance: Explicit waits return as soon as their condition succeeds. A shorter polling interval can react faster but increases command traffic.
  • Reliability: Prefer stable test IDs, ARIA states, URLs, and status attributes over brittle class names and fixed sleeps.
  • Timeout design: Keep navigation and condition budgets separate, and collect diagnostics on failure.
  • Resource use: Headless browsers consume CPU and memory, especially with many images, fonts, ads, and third-party scripts. Reuse drivers for related steps while resetting state deliberately.
  • Cost: Selenium is open source, while hosted browsers, CI minutes, proxy traffic, and storage can add operational cost. Measure your own pages and concurrency.

11. Or skip the browser setup

If the goal is a clean screenshot rather than browser interaction, ScreenshotNeo provides a single GET request. It accepts consent banners as a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

A clean capture can remove common consent and overlay elements before the image is billed.
A clean capture can remove common consent and overlay elements before the image is billed.

See the ScreenshotNeo API documentation for all options.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo supports full-page capture with lazy images loaded, CSS-selector element capture, dark mode, device presets, custom viewports, retina scale, PDF options, HTML/CSS-to-image, custom CSS and JavaScript, clicks, selector/delay/network-idle waits, request blocking, headers, cookies, user agents, Authorization, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, async jobs with signed webhooks, bulk capture of 100 URLs per call, a usage API, and an OpenAPI spec.

Plans include 1,000 shots per month free with no card; Starter is $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan. Create a free ScreenshotNeo account to get started.

12. FAQ

Does driver.get() wait for AJAX?

No. Wait for the AJAX result or application state you need.

Should I always use normal?

Use it for conventional navigation. Choose eager or none only with reliable explicit readiness conditions.

Is time.sleep() acceptable?

Only for a deliberate external pause. Use a condition for page synchronization.

Can page-load strategy vary by URL?

No. It is session-wide; create another driver if necessary.

What timeout should I choose?

Choose values from supported environments and keep navigation and condition budgets separate.