Updated in July 2026 to combine the original two-part tutorial and use the current Selenium API.

Selenium controls a real browser from code. That makes it useful when a page depends on JavaScript or when you need to reproduce a user action such as clicking a button.

This tutorial builds the smallest useful example in two stages:

  • open a page, wait for a button, and click it;
  • move that behavior into a reusable browser wrapper.

Setup

Install the Selenium Python bindings:

python -m pip install --upgrade selenium

Modern Selenium can usually find or download a compatible browser driver through Selenium Manager, so a recent Chrome installation and webdriver.Chrome() are enough for this example.

Open a page and click a button

We will use a tiny page embedded in the script. Unlike a selector copied from a news site, this example will not break when somebody redesigns a page.

from urllib.parse import quote

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


PAGE = """
<!doctype html>
<html lang="en">
  <body>
    <button id="demo-button" onclick="this.textContent = 'Clicked!'">
      Click me
    </button>
  </body>
</html>
"""

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

try:
    driver.get(f"data:text/html;charset=utf-8,{quote(PAGE)}")

    button = wait.until(
        EC.element_to_be_clickable((By.ID, "demo-button"))
    )
    button.click()
    assert button.text == "Clicked!"
finally:
    driver.quit()

The important part is the explicit wait. Pages do not always become interactive the moment they appear. WebDriverWait repeatedly checks the condition until the button is clickable or the five-second timeout expires.

Locators are pairs such as (By.ID, "demo-button"). IDs are usually easier to read and less brittle than long XPath expressions. Selenium also supports CSS selectors, link text, names, and other locator strategies.

Handle a missing element

Waiting for an element that never appears raises TimeoutException. Catch that specific exception when a timeout is an expected outcome:

try:
    missing_button = wait.until(
        EC.element_to_be_clickable((By.ID, "missing-button"))
    )
except TimeoutException:
    print("The button did not become clickable before the timeout.")

Do not catch every exception here. A broad except could hide a broken selector, a browser crash, or a programming error.

Extract a small browser wrapper

Once several scripts need the same waiting and cleanup behavior, a small class can keep that behavior in one place. The decorator below turns an expected timeout into a False result. If decorators are new to you, see this short decorator example.

from functools import wraps
from urllib.parse import quote

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


def report_timeout(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except TimeoutException:
            print(f"{func.__name__} timed out")
            return False

    return wrapper


class Browser:
    def __init__(self, timeout=5):
        self.driver = webdriver.Chrome()
        self.wait = WebDriverWait(self.driver, timeout)

    def load_html(self, html):
        url = f"data:text/html;charset=utf-8,{quote(html)}"
        self.driver.get(url)

    def load_page(self, address):
        self.driver.get(address)

    @report_timeout
    def click(self, locator):
        element = self.wait.until(EC.element_to_be_clickable(locator))
        element.click()
        return True

    def close(self):
        self.driver.quit()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.close()


with Browser(timeout=5) as browser:
    browser.load_html(PAGE)
    clicked = browser.click((By.ID, "demo-button"))
    print(f"Clicked: {clicked}")

The context manager guarantees that quit() runs when the block ends, even if an assertion or browser action fails. quit() closes the entire WebDriver session; close() on the driver would close only the current window.

This wrapper is intentionally small. In a larger test suite, keep page-specific selectors and actions in separate page objects rather than letting one browser class collect every possible interaction.