Migrating from Selenium and Playwright
If you already automate browsers, most of what you know carries over. This page maps the moves you use every day to their Pydoll equivalents, and points out where Pydoll works differently.
Three things change no matter which tool you come from:
- No webdriver or bundled browser. Pydoll drives the Chrome or Edge already on your machine over the DevTools Protocol. There is no
chromedriverto install or version-match. - No explicit waits.
find()andquery()wait for the element themselves, so theWebDriverWaitandexpected_conditionsdance goes away. - Async by default. Every call is
awaited inside anasync def, started withasyncio.run(). New to that? See Async Python in practice.
From Selenium
| Task | Selenium | Pydoll |
|---|---|---|
| Launch | driver = webdriver.Chrome() |
async with Chrome() as browser: tab = await browser.start() |
| Open a URL | driver.get(url) |
await tab.go_to(url) |
| Find by id | driver.find_element(By.ID, 'q') |
await tab.find(id='q') |
| Find by CSS | driver.find_element(By.CSS_SELECTOR, '.item') |
await tab.query('.item') |
| Find by XPath | driver.find_element(By.XPATH, '//a') |
await tab.query('//a') |
| Find by text | driver.find_element(By.XPATH, "//*[text()='Login']") |
await tab.find(text='Login') |
| Find many | driver.find_elements(By.CSS_SELECTOR, '.item') |
await tab.query('.item', find_all=True) |
| Wait for an element | WebDriverWait(driver, 10).until(EC.presence_of_element_located(...)) |
await tab.find(id='q', timeout=10) |
| Click | el.click() |
await el.click() |
| Type | el.send_keys('text') |
await el.type_text('text') |
| Press a key | el.send_keys(Keys.ENTER) |
await tab.keyboard.press(Key.ENTER) |
| Read text | el.text |
await el.text |
| Read an attribute | el.get_attribute('href') |
el.get_attribute('href') |
| Screenshot | driver.save_screenshot('s.png') |
await tab.take_screenshot('s.png') |
| Run JavaScript | driver.execute_script('return document.title') |
await tab.execute_script('return document.title') |
| Quit | driver.quit() |
leave the async with block, or await browser.stop() |
A Selenium login flow and its Pydoll version:
# Selenium
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://quotes.toscrape.com/login')
driver.find_element(By.ID, 'username').send_keys('tester')
driver.find_element(By.ID, 'password').send_keys('secret')
driver.find_element(By.CSS_SELECTOR, "input[type='submit']").click()
WebDriverWait(driver, 5).until(EC.presence_of_element_located((By.LINK_TEXT, 'Logout')))
driver.quit()
# Pydoll
import asyncio
from pydoll.browser.chromium import Chrome
async def main():
async with Chrome() as browser:
tab = await browser.start()
await tab.go_to('https://quotes.toscrape.com/login')
await (await tab.find(id='username')).type_text('tester')
await (await tab.find(id='password')).type_text('secret')
await (await tab.find(tag_name='input', type='submit')).click()
await tab.find(text='Logout', timeout=5)
asyncio.run(main())
get_attribute is synchronous
Unlike Selenium, where everything on the element is a network round-trip, Pydoll reads attributes from the element it already located, so get_attribute() is a plain method with no await. Text is still awaited (await el.text).
From Playwright
| Task | Playwright | Pydoll |
|---|---|---|
| Launch | browser = await p.chromium.launch(); page = await browser.new_page() |
async with Chrome() as browser: tab = await browser.start() |
| Open a URL | await page.goto(url) |
await tab.go_to(url) |
| Find by CSS | page.locator('.item') |
await tab.query('.item') |
| Find by text | page.get_by_text('Login') |
await tab.find(text='Login') |
| Find by role/label | page.get_by_role('button') |
await tab.find(tag_name='button') |
| Find many | page.locator('.item').all() |
await tab.query('.item', find_all=True) |
| Click | await page.locator('.btn').click() |
await (await tab.find(class_name='btn')).click() |
| Fill an input | await page.fill('#q', 'text') |
await (await tab.find(id='q')).type_text('text') |
| Read text | await page.locator('.title').text_content() |
await (await tab.find(class_name='title')).text |
| Read an attribute | await loc.get_attribute('href') |
el.get_attribute('href') |
| New tab | await context.new_page() |
await browser.new_tab() |
| Screenshot | await page.screenshot(path='s.png') |
await tab.take_screenshot('s.png') |
| Close | await browser.close() |
leave the async with block, or await browser.stop() |
Both are async and both auto-wait, so migration is mostly renaming. The main conceptual difference is what a lookup returns:
- A Playwright locator is lazy: it re-resolves the element every time you act on it.
- A Pydoll
find()/query()returns aWebElementresolved once, then and there. Callfind()again if the page replaced the element.
# Playwright
from playwright.async_api import async_playwright
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page()
await page.goto('https://quotes.toscrape.com')
quote = await page.locator('.quote .text').first.text_content()
print(quote)
await browser.close()
# Pydoll
import asyncio
from pydoll.browser.chromium 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.query('.quote .text')
print(await quote.text)
asyncio.run(main())
Behavior you keep from Playwright, and one you gain
Auto-waiting and async carry over directly. What Pydoll adds is humanize=True on clicks and typing, which moves the cursor along a curved path and types with variable timing. See Human-like interactions.
What's next
- Your first automation: a full flow, from login to typed extraction.
- Element finding: every way to locate elements in Pydoll.
- Staying undetected: the anti-bot story, if that's why you're switching.