Skip to content
Selenium WebDriver: Complete Guide to Browser Automation with Python

Selenium WebDriver: Complete Guide to Browser Automation with Python

DodaTech Updated Jun 20, 2026 7 min read

Selenium WebDriver is a browser automation framework that allows you to write code that controls a web browser — clicking buttons, filling forms, navigating pages — just like a human user would.

What You’ll Learn

  • How to set up Selenium WebDriver with Python and Chrome
  • The five main element locator strategies: ID, name, class, CSS, XPath
  • Implicit vs explicit waits and when to use each
  • The Page Object Model design pattern for maintainable tests
  • Running Selenium tests in headless mode for CI/CD

Why Selenium Matters

Selenium is the most widely used browser automation framework with support for Chrome, Firefox, Safari, and Edge. Despite newer tools like Playwright and Cypress, Selenium remains the standard for cross-browser testing and has the largest community, most documentation, and widest language support. Understanding Selenium is a foundational skill for any test automation engineer.

Doda Browser uses Selenium for cross-platform compatibility testing — ensuring every feature works consistently across Chrome, Firefox, and Safari before each release.

Learning Path

    flowchart LR
  A[Testing Basics] --> B[Selenium Basics<br/>You are here]
  B --> C[Page Object Model]
  C --> D[Test Automation CI/CD]
  D --> E[E2E Testing]
  style B fill:#f90,color:#fff
  

Setup

pip install selenium webdriver-manager
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

# Automatic driver management
driver = webdriver.Chrome(
    service=Service(ChromeDriverManager().install())
)

driver.get("https://example.com")
print(driver.title)  # Example Domain
driver.quit()

Element Locators

Finding elements is the most fundamental skill in Selenium. You have five main strategies:

By ID

Fastest and most reliable — use it when available.

from selenium.webdriver.common.by import By

element = driver.find_element(By.ID, "email")
element.send_keys("user@example.com")

By Name

driver.find_element(By.NAME, "password").send_keys("secret123")

By Class Name

# Finds first element with class "error-message"
error = driver.find_element(By.CLASS_NAME, "error-message")
print(error.text)

By CSS Selector

Most flexible for complex queries:

# CSS selector examples
driver.find_element(By.CSS_SELECTOR, "button[type='submit']")
driver.find_element(By.CSS_SELECTOR, ".nav-item.active")
driver.find_element(By.CSS_SELECTOR, "#login-form input:first-child")

By XPath

Most powerful, but slower and more brittle:

# XPath examples
driver.find_element(By.XPATH, "//button[text()='Submit']")
driver.find_element(By.XPATH, "//div[@class='error']/span")
driver.find_element(By.XPATH, "//input[@type='email' and @required]")

Preference order: ID > Name > CSS > XPath. Use XPath only when CSS selectors can’t express the relationship.

Waits

Web pages load asynchronously. Elements may not be available immediately. Waits solve this.

Implicit Waits

Poll the DOM for a set duration before raising an exception:

driver.implicitly_wait(10)  # Wait up to 10 seconds

Caveat: Applies to all find_element calls. Can slow down tests when elements are genuinely missing.

Explicit Waits

Wait for a specific condition on a specific element:

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10)

# Wait for element to be visible
element = wait.until(
    EC.visibility_of_element_located((By.ID, "success-message"))
)

# Wait for element to be clickable
button = wait.until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, "button[type='submit']"))
)

# Wait for URL to change
wait.until(EC.url_contains("/dashboard"))

# Wait for element to contain text
wait.until(
    EC.text_to_be_present_in_element((By.ID, "status"), "Completed")
)

When to Use Each

Wait TypeWhen to Use
ImplicitSimple scripts, consistent behavior
ExplicitWhen you need precise conditions
Fixed sleepNever — it’s a code smell. Use waits instead

Full Example: Login Test

import pytest
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

class TestLogin:
    def setup_method(self):
        self.driver = webdriver.Chrome()
        self.driver.get("https://example.com/login")
        self.wait = WebDriverWait(self.driver, 10)

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

    def test_successful_login(self):
        # Fill in credentials
        self.driver.find_element(By.ID, "email").send_keys("user@example.com")
        self.driver.find_element(By.ID, "password").send_keys("secret123")

        # Submit
        self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

        # Wait for dashboard
        self.wait.until(EC.url_contains("/dashboard"))
        welcome = self.driver.find_element(By.CLASS_NAME, "welcome")
        assert "Welcome" in welcome.text

    def test_invalid_credentials(self):
        self.driver.find_element(By.ID, "email").send_keys("wrong@example.com")
        self.driver.find_element(By.ID, "password").send_keys("wrongpass")
        self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

        error = self.wait.until(
            EC.visibility_of_element_located((By.CLASS_NAME, "error-message"))
        )
        assert "Invalid" in error.text

Page Object Model (POM)

POM is the most important design pattern for Selenium tests. Each page becomes a class that encapsulates its elements and interactions:

# pages/login_page.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class LoginPage:
    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)

    # Locators
    EMAIL_INPUT = (By.ID, "email")
    PASSWORD_INPUT = (By.ID, "password")
    SUBMIT_BUTTON = (By.CSS_SELECTOR, "button[type='submit']")
    ERROR_MESSAGE = (By.CLASS_NAME, "error-message")

    # Actions
    def enter_email(self, email):
        self.driver.find_element(*self.EMAIL_INPUT).send_keys(email)

    def enter_password(self, password):
        self.driver.find_element(*self.PASSWORD_INPUT).send_keys(password)

    def click_submit(self):
        self.driver.find_element(*self.SUBMIT_BUTTON).click()

    def login(self, email, password):
        self.enter_email(email)
        self.enter_password(password)
        self.click_submit()

    def get_error_message(self):
        return self.wait.until(
            EC.visibility_of_element_located(self.ERROR_MESSAGE)
        ).text

# tests/test_login.py
from pages.login_page import LoginPage
from pages.dashboard_page import DashboardPage

class TestLogin:
    def test_successful_login(self, driver):
        login_page = LoginPage(driver)
        login_page.login("user@example.com", "secret123")

        dashboard = DashboardPage(driver)
        assert dashboard.is_displayed()
        assert "Welcome" in dashboard.get_welcome_text()

Benefits of POM

  • Reduced duplication: Selectors defined once
  • Easier maintenance: When a page changes, update one class
  • Readability: Tests read like user actions
  • Reusability: Page objects can be used across multiple tests

Headless Mode for CI/CD

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

def create_driver():
    options = Options()
    options.add_argument("--headless")        # No GUI
    options.add_argument("--no-sandbox")       # Required for Docker
    options.add_argument("--disable-dev-shm-usage")  # Shared memory fix
    options.add_argument("--window-size=1920,1080")
    return webdriver.Chrome(options=options)

Common Selenium Mistakes

1. Using sleep() Instead of Waits

Fixed waits make tests slow and flaky. A page that loads in 2 seconds one day and 5 seconds the next will break your sleep-based tests.

Fix: Always use explicit waits with expected conditions.

2. Brittle XPath Selectors

XPaths like //div[3]/span[2]/input break when the DOM structure changes even slightly.

Fix: Use ID, name, or CSS selectors. If you must use XPath, use text-based or attribute-based expressions.

3. Not Cleaning Up Test Data

Tests that create users but don’t delete them fill up the database.

Fix: Use setup/teardown methods to clean up after each test.

4. Testing on the Wrong Environment

Running tests against production risks corrupting data.

Fix: Use dedicated staging or test environments. Use test accounts with limited permissions.

5. No Page Object Model

Tests with duplicated selectors are hard to maintain and painful to update.

Fix: Always use POM for test suites of more than 10 tests.

6. Not Handling StaleElementReferenceException

This exception occurs when a previously located element is no longer in the DOM (page refreshed, element re-rendered).

Fix: Re-locate the element before interacting with it after a page change.

7. Running Tests Sequentially When They Could Run in Parallel

Selenium Grid or parallel test execution can dramatically reduce test suite time.

Fix: Use pytest-xdist for parallel test execution or Selenium Grid for distributed testing.

Practice Questions

1. What are the five main element locator strategies in Selenium?

ID, name, class name, CSS selector, and XPath.

2. What is the difference between implicit and explicit waits?

Implicit waits apply to all find_element calls with a global timeout. Explicit waits apply to specific elements with specific conditions.

3. What is the Page Object Model design pattern?

A pattern where each web page is represented by a class that encapsulates its element locators and interaction methods.

4. Why should you avoid time.sleep() in Selenium tests?

Fixed sleeps make tests slow (worst-case wait time) and brittle (varying load times break them). Explicit waits adapt to actual page timing.

5. How do you run Selenium tests in headless mode?

Configure Chrome options with --headless argument. This is essential for running tests in CI/CD environments without a display.

Challenge: Create a Page Object Model test suite for a real website. Implement at least three page objects (e.g., Login, Dashboard, Profile) with interaction methods and verify end-to-end flow.

FAQ

Can Selenium test mobile browsers?
Not directly. Selenium automates desktop browsers. Use Appium (which extends the WebDriver protocol) for mobile testing.
What is a StaleElementReferenceException?
It occurs when you try to interact with an element that is no longer in the DOM. Re-locate the element before using it.
Should I use CSS selectors or XPath?
CSS selectors are faster and more readable. Use XPath only when you need to traverse the DOM (parent elements, text-based selection).
How do I handle file uploads in Selenium?
Use send_keys(file_path) on the file input element. This bypasses the OS file dialog.
Can I run Selenium tests in parallel?
Yes, using pytest-xdist for local parallel execution or Selenium Grid for distributed execution across machines.

What’s Next

TutorialWhat You’ll Learn
Page Object Model Best PracticesAdvanced POM patterns and utilities
Playwright Browser TestingModern browser automation alternatives
Test Automation FrameworksComparing test automation frameworks

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-20.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro