Selenium Page Object Model: How Centralizing Locators Keeps Your Tests From Crashing
Learn how Selenium’s Page Object Model centralizes element locators and encapsulates actions to keep tests maintainable. A concrete example, trade‑offs, and actionable steps show why POM is a practical engineering decision.
21 Feb 2026, 07:15 UTC

The Problem: Brittle Selectors in Selenium Tests
When you write a Selenium test, you’re usually sprinkling driver.find_element(By.ID, "login-btn") all over your test suite. If the UI changes – a new class name, a moved element – every test that references that string breaks. The result is a maintenance nightmare: you hunt through dozens of files for a single selector, refactor it, and run the entire suite again to make sure nothing else broke.
The Thesis: Page Object Model Encapsulates Selectors and Actions
The Page Object Model (POM) is Selenium’s officially supported pattern for reducing that brittleness. A page class holds all the locators for a page and exposes high‑level methods that mirror user actions. Test code then calls those methods instead of directly interacting with WebDriver.
- Centralizes element locators – one change in the page class propagates everywhere.
- Encapsulates actions – tests read like a story, not a list of low‑level clicks.
- Separates concerns – page logic lives in the page class, test logic lives in the test class.
How to Structure a Page Object
- Define a clear class name – e.g.,
LoginPageorAccountSettingsPage. - Store locators as class attributes – use
Byobjects or tuples so they’re easy to reference.class LoginPage: _username_input = (By.ID, "username") _password_input = (By.ID, "password") _login_button = (By.CSS_SELECTOR, "button[type='submit']") - Wrap interactions in methods – keep each method small and focused.
def login(self, username, password): WebDriverWait(self.driver, 10).until( EC.visibility_of_element_located(self._username_input)) self.driver.find_element(*self._username_input).send_keys(username) self.driver.find_element(*self._password_input).send_keys(password) self.driver.find_element(*self._login_button).click() - Expose only what the test needs – avoid leaking
WebDriverto the test layer. - Use a constructor that accepts a
WebDriverinstance.def __init__(self, driver): self.driver = driver
Worked Example: Login Flow With and Without POM
Below is a minimal test that logs into a demo site. First, the “raw” approach. Then the same test rewritten to use the LoginPage POM class.
Without POM (12 lines of locator logic)
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()
driver.get("https://example.com/login")
WebDriverWait(driver, 10).until(
EC.visibility_of_element_located((By.ID, "username")))
driver.find_element(By.ID, "username").send_keys("user")
driver.find_element(By.ID, "password").send_keys("pass")
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
With POM (8 lines – all locators hidden)
class LoginPage:
_username_input = (By.ID, "username")
_password_input = (By.ID, "password")
_login_button = (By.CSS_SELECTOR, "button[type='submit']")
def __init__(self, driver):
self.driver = driver
def login(self, username, password):
WebDriverWait(self.driver, 10).until(
EC.visibility_of_element_located(self._username_input))
self.driver.find_element(*self._username_input).send_keys(username)
self.driver.find_element(*self._password_input).send_keys(password)
self.driver.find_element(*self._login_button).click()
# Test
from selenium import webdriver
driver = webdriver.Chrome()
driver.get("https://example.com/login")
login_page = LoginPage(driver)
login_page.login("user", "pass")
Notice the test now reads login_page.login(...) – a single line that tells the story of the test. If the username field changes its ID, you update it once in LoginPage and all tests stay green.
Trade‑Offs & Limitations
- Over‑abstraction – If a page has only two elements, a POM class might be overkill. In small projects, a simple helper module can suffice.
- Granularity – Splitting a page into dozens of tiny classes can bloat the codebase and make navigation harder. Aim for one class per logical page or section.
- Debugging – When a test fails inside a POM method, the stack trace points to the method, not the exact locator line. Adding meaningful log statements or returning the locator used can help.
- Learning curve – New team members must understand the pattern before they can contribute. Pair‑programming and documentation help.
- Performance – The overhead of method calls and explicit waits is negligible compared to network latency.
Actionable Guidance for Your Team
- Start small – Create a POM class for the most frequently used page (e.g., login). Measure locator duplication with a static analysis tool (e.g.,
grep -r "By."). - Enforce naming conventions – Use lowercase with underscores for locators (
_search_input) and PascalCase for classes. - Centralize waits – All POM methods should use
WebDriverWaitfor dynamic elements. Avoid hardtime.sleepcalls. - Document public API – Add docstrings that describe what the test writer sees, not the underlying locator.
- Review periodically – During sprint retrospectives, check if any POM class has grown beyond 200 LOC; if so, consider refactoring.
- Automate duplication checks – A simple script can count unique locator strings across the repo; flag any that appear more than once.
By adopting POM, you trade a few extra lines of code for a dramatic reduction in maintenance pain. If your test suite is already stable and small, you can skip POM, but for any growing project, the benefits outweigh the modest overhead.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.