Element finding
Locating elements is the foundation of every automation. Pydoll gives you two ways to do it: find(), where you describe the element by its HTML attributes, and query(), where you pass a CSS selector or XPath. Both wait for the element to appear, so you never write manual sleep loops.
Edit the attributes below and watch find() locate the element live. Pydoll turns the attributes you pass into a selector for you, and the matching element lights up.
Find by attributes
find() is the everyday tool. You pass the attributes you'd use to describe the element to a person, and Pydoll builds the selector for you.
import asyncio
from pydoll import Chrome
async def main():
async with Chrome() as browser:
tab = await browser.start()
await tab.go_to('https://quotes.toscrape.com')
quote = await tab.find(class_name='quote')
text = await quote.find(class_name='text')
author = await quote.find(class_name='author')
print(f'{await author.text()}: {await text.text()}')
asyncio.run(main())
You can locate an element by any of these attributes. Each of these returns the first match:
Combine attributes for precision
Pass several attributes and find() matches the element that has all of them (an AND). Use underscores for hyphenated attribute names: data-testid becomes data_testid, aria-label becomes aria_label.
# an <input type="password" name="password">
password = tab.find(tag_name='input', type='password', name='password')
# a <button class="btn" type="submit">
submit = tab.find(tag_name='button', class_name='btn', type='submit')
# a data attribute
card = tab.find(tag_name='div', data_testid='product-card')
# an <input type="password" name="password">
password = await tab.find(tag_name='input', type='password', name='password')
# a <button class="btn" type="submit">
submit = await tab.find(tag_name='button', class_name='btn', type='submit')
# a data attribute
card = await tab.find(tag_name='div', data_testid='product-card')
For OR logic (the element might have one attribute or another), chain two calls with raise_exc=False, shown under Handle missing elements.
Find every match
Pass find_all=True to get a list of every matching element instead of the first one:
await tab.go_to('https://books.toscrape.com')
books = await tab.find(class_name='product_pod', find_all=True)
print(f'{len(books)} books on this page')
for book in books:
title = await book.find(tag_name='h3')
price = await book.find(class_name='price_color')
print(await title.text(), await price.text())
Wait for elements that load late
Modern pages render content after the initial load. Pass timeout (in seconds) and find() polls until the element appears or the time runs out. You don't add sleep calls; the wait is built in.
Pick timeouts deliberately
Too short and you miss slow elements; too long and you wait on things that will never appear. Five to ten seconds fits most dynamic content. For an element that is only sometimes present, pair a short timeout with raise_exc=False (below).
Find by CSS selector or XPath
When you already have a selector, or need a relationship find() can't express, use query(). It auto-detects CSS versus XPath.
# CSS
submit = tab.query("button[type='submit']")
required = tab.query('input[required]', find_all=True)
nested = tab.query('div.container > .content .item:nth-child(2)')
# XPath: text matching and relationships CSS can't reach
button = tab.query("//button[contains(text(), 'Submit')]")
label_input = tab.query("//label[text()='Email:']/following-sibling::input")
# CSS
submit = await tab.query("button[type='submit']")
required = await tab.query('input[required]', find_all=True)
nested = await tab.query('div.container > .content .item:nth-child(2)')
# XPath: text matching and relationships CSS can't reach
button = await tab.query("//button[contains(text(), 'Submit')]")
label_input = await tab.query("//label[text()='Email:']/following-sibling::input")
query() takes the same find_all, timeout, and raise_exc parameters as find(). For when to reach for CSS versus XPath, see Selectors: CSS and XPath.
Search within an element
Every element supports find() and query() scoped to its own subtree, which is how you work with repeating structures like cards or rows. A scoped search looks through all descendants of the element, not only its direct children, matching how querySelector behaves.
To navigate the DOM tree deliberately (direct children only, siblings, shadow roots), see DOM traversal.
Handle missing elements
By default find() raises ElementNotFound when nothing matches. Pass raise_exc=False to get None instead, which keeps optional elements and OR logic in your hands.
# required element: let it raise
submit = tab.find(id='submit')
# optional element: handle the None
banner = tab.find(class_name='promo-banner', timeout=2, raise_exc=False)
if banner:
close = banner.find(class_name='close')
close.click()
# OR logic: try one attribute, then another
checkbox = (
tab.find(id='terms', raise_exc=False)
or tab.find(name='accept_terms', raise_exc=False)
)
# required element: let it raise
submit = await tab.find(id='submit')
# optional element: handle the None
banner = await tab.find(class_name='promo-banner', timeout=2, raise_exc=False)
if banner:
close = await banner.find(class_name='close')
await close.click()
# OR logic: try one attribute, then another
checkbox = (
await tab.find(id='terms', raise_exc=False)
or await tab.find(name='accept_terms', raise_exc=False)
)
Prefer stable selectors
Choose attributes a redesign is unlikely to change. Your DOM structure shifts often, so selectors that depend on it break easily.
Reach for the simplest selector that works, and only add complexity when the page forces it. Use find() for attribute-based lookups and query() for CSS or XPath patterns find() can't express.
Complete example: log in and read the result
This logs in on quotes.toscrape.com (which accepts any credentials) and confirms the result by finding the Logout link.
from pydoll.sync import Chrome
def main():
with Chrome() as browser:
tab = browser.start()
tab.go_to('https://quotes.toscrape.com/login')
username = tab.find(id='username')
username.type_text('tester', humanize=True)
password = tab.find(id='password')
password.type_text('secret', humanize=True)
submit = tab.find(tag_name='input', type='submit')
submit.click()
logout = tab.find(text='Logout', timeout=5, raise_exc=False)
print('Logged in.' if logout else 'Login failed.')
main()
import asyncio
from pydoll import Chrome
async def main():
async with Chrome() as browser:
tab = await browser.start()
await tab.go_to('https://quotes.toscrape.com/login')
username = await tab.find(id='username')
await username.type_text('tester', humanize=True)
password = await tab.find(id='password')
await password.type_text('secret', humanize=True)
submit = await tab.find(tag_name='input', type='submit')
await submit.click()
logout = await tab.find(text='Logout', timeout=5, raise_exc=False)
print('Logged in.' if logout else 'Login failed.')
asyncio.run(main())
What's next
- DOM traversal: navigate from an element to its children, siblings, and shadow roots.
- Selectors: CSS and XPath: choose and write the right selector.
- Structured extraction: pull typed data from many elements at once with a model.