As your page object collection grows, you will notice every page object contains the same boilerplate: a constructor that stores the driver, a WebDriverWait instance, a method to click elements safely, a method to type text with clearing, and a method to wait for elements. The BasePage pattern extracts this shared logic into a parent class that all page objects inherit. This eliminates duplication, standardises wait behaviour, and makes adding new page objects trivially fast.
The BasePage — Foundation of Every Page Object
BasePage provides three categories of shared functionality: driver and wait management, wrapped element interactions (click, type, read), and utility methods (screenshot, scroll, URL check).
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.common.exceptions import TimeoutException
import os
class BasePage:
def __init__(self, driver, timeout=10):
self.driver = driver
self.wait = WebDriverWait(driver, timeout)
# ── Element interaction wrappers ──
def find(self, locator):
return self.wait.until(EC.visibility_of_element_located(locator))
def find_all(self, locator):
self.wait.until(EC.presence_of_element_located(locator))
return self.driver.find_elements(*locator)
def click(self, locator):
element = self.wait.until(EC.element_to_be_clickable(locator))
element.click()
def type_text(self, locator, text):
element = self.find(locator)
element.clear()
element.send_keys(text)
def get_text(self, locator):
return self.find(locator).text
def is_visible(self, locator, timeout=3):
try:
WebDriverWait(self.driver, timeout).until(
EC.visibility_of_element_located(locator)
)
return True
except TimeoutException:
return False
# ── Navigation utilities ──
def get_current_url(self):
return self.driver.current_url
def get_title(self):
return self.driver.title
def wait_for_url_contains(self, text):
self.wait.until(EC.url_contains(text))
# ── Utility methods ──
def take_screenshot(self, name="screenshot"):
os.makedirs("reports/screenshots", exist_ok=True)
path = f"reports/screenshots/{name}.png"
self.driver.save_screenshot(path)
return path
def scroll_to_element(self, locator):
element = self.driver.find_element(*locator)
self.driver.execute_script(
"arguments[0].scrollIntoView({behavior:'smooth',block:'center'});",
element
)
# ── LoginPage inherits from BasePage ──
class LoginPage(BasePage):
URL = "https://www.saucedemo.com"
USERNAME_INPUT = (By.ID, "user-name")
PASSWORD_INPUT = (By.ID, "password")
LOGIN_BUTTON = (By.ID, "login-button")
ERROR_MESSAGE = (By.CSS_SELECTOR, "[data-test='error']")
def open(self):
self.driver.get(self.URL)
self.wait.until(EC.visibility_of_element_located(self.LOGIN_BUTTON))
return self
def login(self, username, password):
self.type_text(self.USERNAME_INPUT, username) # inherited from BasePage
self.type_text(self.PASSWORD_INPUT, password) # inherited from BasePage
self.click(self.LOGIN_BUTTON) # inherited from BasePage
return InventoryPage(self.driver)
def login_expecting_error(self, username, password):
self.type_text(self.USERNAME_INPUT, username)
self.type_text(self.PASSWORD_INPUT, password)
self.click(self.LOGIN_BUTTON)
return self
def get_error_message(self):
return self.get_text(self.ERROR_MESSAGE) # inherited from BasePage
# ── InventoryPage inherits from BasePage ──
class InventoryPage(BasePage):
INVENTORY_LIST = (By.CLASS_NAME, "inventory_list")
INVENTORY_ITEM = (By.CLASS_NAME, "inventory_item")
ITEM_NAME = (By.CLASS_NAME, "inventory_item_name")
def __init__(self, driver):
super().__init__(driver)
self.wait.until(EC.visibility_of_element_located(self.INVENTORY_LIST))
def get_product_count(self):
return len(self.find_all(self.INVENTORY_ITEM)) # inherited from BasePage
def get_product_names(self):
elements = self.find_all(self.ITEM_NAME) # inherited from BasePage
return [el.text for el in elements]
# ── Compare: without BasePage vs with BasePage ──
print("Without BasePage — each page object repeats:")
print(" self.driver = driver")
print(" self.wait = WebDriverWait(driver, 10)")
print(" WebDriverWait(driver,10).until(EC.visibility_of...)")
print(" element.clear(); element.send_keys(text)")
print()
print("With BasePage — page objects call:")
print(" self.type_text(locator, text)")
print(" self.click(locator)")
print(" self.get_text(locator)")
print(" self.is_visible(locator)")
print()
print("Result: LoginPage is 20 lines instead of 45. Zero duplicated wait logic.")
find() method in BasePage uses visibility_of_element_located rather than presence_of_element_located. Presence means the element exists in the DOM (even if hidden). Visibility means it exists AND is displayed to the user. For interaction methods (click, type), you almost always want visibility — clicking a hidden element throws an exception, and typing into a hidden field has no visible effect. Use presence only when you need to check that an element was loaded into the DOM regardless of its display state.take_screenshot method to BasePage and call it automatically when a test fails. In pytest, you can use a fixture that checks the test result and captures a screenshot on failure: if request.node.rep_call.failed: page.take_screenshot(request.node.name). These failure screenshots are invaluable for debugging CI/CD failures where you cannot see the browser.verify_login_success() to BasePage, that logic belongs in LoginPage or in the test itself. BasePage is infrastructure; page objects are application-specific; tests are business-rule-specific. Keep these three layers distinct.Common Mistakes
Mistake 1 — Not using a BasePage, duplicating wait logic in every page object
❌ Wrong: Every page object has its own WebDriverWait setup, its own try/except TimeoutException blocks, and its own element.clear(); element.send_keys() pattern — duplicated 15 times across 15 page objects.
✅ Correct: BasePage defines these once. Page objects call self.type_text(locator, text) and inherit all the wait and interaction logic.
Mistake 2 — Making BasePage too specific to one application
❌ Wrong: BasePage contains methods like login(), add_to_cart(), and checkout() — application-specific actions that do not belong in a generic base class.
✅ Correct: BasePage contains only generic browser actions: click(), type_text(), get_text(), is_visible(), take_screenshot(). Application-specific actions belong in their respective page objects.