Not long ago I ran a small poll: who's still using Selenium in 2025?
Surprisingly most respondents said they are.
These days, I mostly work on backend tests, with just a bit of UI covered using Playwright.
But that poll brought back a technique we used with Selenium around seven years ago.
It's about using descriptors in PageObjects - a rare but elegant approach that still holds up today.
I'm not presenting this as the only right way, but rather as a small practice that once helped — and might be worth revisiting.
A quick refresher: what are descriptors in Python?
In simple terms, a descriptor is any object that implements one of the methods: __get__
, __set__
, or __delete__
.
They’re the mechanism behind things like @property
, but you can use them directly to control attribute access in custom ways.
Here’s a minimal example of how it works:
class LoggedAttribute:
def __get__(self, instance, owner):
print(f"Accessing attribute via __get__: instance={instance}, owner={owner}")
return instance._value
def __set__(self, instance, value):
print(f"Setting attribute via __set__: instance={instance}, value={value}")
instance._value = value
class MyClass:
value = LoggedAttribute()
def __init__(self, value):
self.value = value # Triggers __set__
obj = MyClass(10)
print(obj.value) # Triggers __get__
In this example, value
is not a regular attribute — it’s a descriptor.
Whenever you read or write obj.value
, Python routes the access through the descriptor’s __get__
and __set__
methods.
Inside __get__
, you get access to:
-
instance
— the actual object (obj
), so you can fetch instance-specific data, -
owner
— the class (MyClass
), in case you need class-level logic.
This is exactly what makes descriptors powerful:
they give you full control over attribute access — which, as I later learned, maps beautifully to things like lazy-loaded UI elements.
Back then I was a pretty green junior QAA, and this technique was already implemented in the project I joined —
but it immediately caught my attention. It felt like magic: clean, reusable, and somehow more Pythonic than anything I'd seen before.
Years later, I still think it’s an underrated trick, especially for building PageObjects that don’t rely on hard-coded find_element
calls.
That small mechanism — __get__
, and optionally __set__
— gives you precise control over how attributes behave.
And while most people encounter descriptors through @property
, they can be used much more creatively.
One such case?
Injecting dynamic behavior into PageObjects — like locating elements only when they’re accessed.
Instead of writing repetitive self.driver.find_element(...)
calls in every page method, we wrapped those lookups into descriptors.
It looked something like this:
class BasePage:
driver: WebDriver
accept_all_cookies_btn: Element = Element(By.CLASS_NAME, "fc-cta-consent")
def __init__(self, driver: WebDriver):
self.driver = driver
def accept_all_cookies(self) -> Self:
self.accept_all_cookies_btn.click()
return self
def is_page_opened(self) -> bool:
pass
class ATEHomePage(BasePage):
logo: Element = Element(By.CSS_SELECTOR, "img[src='/static/images/home/logo.png']")
def __init__(self, driver: WebDriver):
super().__init__(driver)
def is_page_opened(self) -> bool:
return self.logo.is_displayed()
class Element:
def __init__(self, by: By, value: str, wait: bool=False, timeout: int=10):
self.by = by
self.value = value
self.wait = wait
self.timeout = timeout
def __get__(self, instance, owner) -> Self | WebElement:
if instance is None:
return self
return self._find(instance)
def _find(self, instance) -> WebElement:
driver: WebDriver = instance.driver
if self.wait:
wait = WebDriverWait(driver, self.timeout)
return wait.until(EC.presence_of_element_located((str(self.by), self.value)))
return driver.find_element(self.by, self.value)
def exists(self, instance) -> bool:
try:
instance.driver.find_element(self.by, self.value)
return True
except NoSuchElementException:
return False
def is_displayed(self, instance) -> bool:
try:
return self._find(instance).is_displayed()
except (NoSuchElementException, StaleElementReferenceException):
return False
def text(self, instance) -> str | None:
try:
return self._find(instance).text
except (NoSuchElementException, StaleElementReferenceException):
return None
Let’s see how this works in practice with a simple scenario:
start_page: ATEHomePage = ATEHomePage(driver)
start_page.accept_all_cookies()
assert start_page.is_page_opened(), "Start page is not opened."
What happens behind the scenes when, for example, start_page.is_page_opened() calls self.logo.is_displayed()?
- Accessing start_page.logo triggers the descriptor’s get method.
- Inside get, the descriptor receives instance — the start_page object — which has a driver attribute.
- Using instance.driver, the descriptor locates the element on the page (driver.find_element(...)).
- The located WebElement is returned.
- Finally, .is_displayed() is called on that WebElement.
Here’s a simplified visual of this flow:
Why use descriptors in PageObjects? The benefits
Lazy evaluation: Elements are located only when accessed, not upfront. This speeds up page initialization and reduces errors related to missing elements.
DRY (Don’t Repeat Yourself): The repetitive element lookup code is centralized in one place — the descriptor. You don’t have to write
self.driver.find_element(...)
all over your PageObject methods.Clear and concise PageObjects: Page attributes look like regular properties, not methods with lookup logic — making your code more readable and declarative.
Single point of control: Need to change the lookup strategy, add waiting logic, or handle exceptions? Do it once inside the descriptor — and all pages inherit the updated behavior automatically.
Encapsulation of UI logic: You can extend functionality — like adding
is_displayed()
,exists()
,text()
methods — directly in the descriptor, rather than spreading them across PageObjects.
This approach effectively makes your PageObject attributes “smart” — they behave like regular fields but carry behind-the-scenes logic to interact with the UI in a robust and maintainable way.
The complete descriptor-based PageObject example is available on my GitHub:
https://github.com/shukal94/getdescriptor
The code is ready to run: you just need to have Python installed and set up the dependencies.
Feel free to clone the repo and try it out yourself!
I’d love to hear your feedback - let me know if it works well for you or if you have any questions.
If you found this helpful, be sure to check out the featured posts on my profile. Originally shared on LinkedIn, here's a more detailed write-up with code and diagrams. Also if you’re working on improving your framework design or want to see how I teach these patterns in depth — feel free to check out my course collection on Udemy
Stay tuned and follow along!
Top comments (0)