TL;DR: Both Playwright and Selenium can drive real browsers for scraping and automation. In 2026, Playwright is usually the better default for new Python scrapers (auto-waits, tracing, multi-browser API). Selenium still wins when you must plug into an existing Grid, WebDriver-only stack, or a mature internal suite. Below is a practical comparison with working snippets—not a benchmark horse race.
What “production scraping” actually means
A toy script that dumps one page is not production. Production means:
- Runs unattended for hours or on a schedule
- Survives timeouts, layout shifts, and partial failures
- Emits structured, deduplicated data
- Is observable (logs, traces, screenshots on failure)
- Stays inside authorized use (public pages or permissioned sessions)
Both libraries can do that. The difference is how much glue you write.
Quick side-by-side
| Concern | Playwright (Python) | Selenium 4 (Python) |
|---|---|---|
| Install / browsers |
playwright install manages browser builds |
Drivers via Selenium Manager (improved a lot) |
| Auto-wait | Built into locators | Explicit waits (WebDriverWait) are the norm |
| Multi-browser API | Chromium, Firefox, WebKit in one API | Strong Chrome/Firefox; WebKit via other stacks |
| Parallelism model | Browser contexts are cheap and isolated | Classic: many WebDriver sessions / Grid nodes |
| Debugging | Trace viewer, video, inspector | Screenshots + browser DevTools; tracing via extras |
| Legacy Fit | Newer codebases | Huge existing Grid / enterprise suites |
None of these rows is a claim about “X is N% faster.” Measure on your pages.
Waiting: the everyday difference
Most scraper bugs are race conditions. Playwright’s locators retry until actionable (or timeout). Selenium expects you to declare waits.
Playwright:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://example.com/search", wait_until="domcontentloaded")
page.fill('input[name="q"]', "MC123456")
page.click('button[type="submit"]')
# Auto-waits for the row to be visible before reading text
name = page.locator(".result-row >> nth=0 >> .legal-name").inner_text()
print(name)
browser.close()
Selenium 4:
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()
wait = WebDriverWait(driver, 20)
driver.get("https://example.com/search")
wait.until(EC.presence_of_element_located((By.NAME, "q"))).send_keys("MC123456")
driver.find_element(By.CSS_SELECTOR, 'button[type="submit"]').click()
name = wait.until(
EC.visibility_of_element_located((By.CSS_SELECTOR, ".result-row .legal-name"))
).text
print(name)
driver.quit()
Same intent; Selenium is more verbose. That verbosity is fine when your team already lives in WebDriver patterns.
Isolation: contexts vs many drivers
Playwright browser contexts are lightweight profiles: cookies, storage, and permissions stay isolated without launching a full browser per job.
from playwright.sync_api import sync_playwright
def scrape_urls(urls: list[str]) -> list[str]:
titles = []
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
for url in urls:
context = browser.new_context(
user_agent="INDUSResearchBot/1.0 (+https://www.induswebagency.com)"
)
page = context.new_page()
page.goto(url, wait_until="domcontentloaded", timeout=45_000)
titles.append(page.title())
context.close() # drop cookies/storage for that job
browser.close()
return titles
With Selenium you typically create multiple WebDriver sessions (locally or on Grid). That works at scale, but process overhead and session management are on you (or your Grid ops).
Network control and selective blocking
Scrapers often do not need images, fonts, or analytics. Playwright makes route interception straightforward:
def block_noise(page) -> None:
def _handler(route):
if route.request.resource_type in {"image", "media", "font"}:
return route.abort()
if "google-analytics.com" in route.request.url:
return route.abort()
return route.continue_()
page.route("**/*", _handler)
Selenium can do CDP-level control in Chromium, but the happy path is less uniform across browsers. If your scraper is Chromium-only and you already know CDP, Selenium is workable; for cross-browser request surgery, Playwright is usually less ceremony.
Debugging a failed production run
When a nightly job fails at 3 a.m., you want artifacts.
Playwright tracing (sketch):
context = browser.new_context()
context.tracing.start(screenshots=True, snapshots=True, sources=True)
page = context.new_page()
try:
run_job(page)
finally:
context.tracing.stop(path="trace.zip")
# open with: playwright show-trace trace.zip
Selenium practical minimum:
from pathlib import Path
def save_failure(driver, job_id: str) -> None:
Path("artifacts").mkdir(exist_ok=True)
driver.save_screenshot(f"artifacts/{job_id}.png")
Path(f"artifacts/{job_id}.html").write_text(driver.page_source, encoding="utf-8")
Traces with DOM snapshots save hours compared to a single PNG. If your org already standardized on Selenium screenshots + logging, keep that—just make failure artifacts mandatory.
When Selenium is still the right call
Choose Selenium when:
- You already run Selenium Grid (or a vendor Grid) with capacity and runbooks
- Compliance or IT only blesses WebDriver-based automation
- The task is less “scrape public HTML” and more “drive an internal WebDriver test stack” that already owns page objects
- A client’s codebase is Selenium and rewriting would cost more than shipping the feature
Choose Playwright when:
- You are starting a new scraper or automation service in 2026
- You want auto-waits, tracing, and multi-browser APIs without extra frameworks
- You need many isolated sessions (contexts) on one machine for authorized jobs
A note on ethics and ToS
Tool choice does not change the rules. Neither Playwright nor Selenium justifies bypassing authentication, CAPTCHAs, or rate limits. Prefer official APIs when they exist; scrape public pages only within terms and permission; identify your traffic; store only what you need.
Migration sketch (Selenium → Playwright)
You do not need a big-bang rewrite. Migrate job by job:
- Keep the sink (DB/CSV) and normalizers
- Replace the fetch layer first
- Re-map CSS/XPath to Playwright locators (
get_by_role,locator) - Add tracing on the new path only
- Compare outputs on a golden set of URLs before cutting traffic over
FAQ
Q: Is Playwright “replacing” Selenium in 2026?
A: For many new scraping and UI-automation greenfields, Playwright is the default recommendation. Selenium remains widely deployed and actively maintained. “Replacement” depends on your constraints, not a slogan.
Q: Which is faster?
A: It depends on the page, headless settings, resource blocking, and whether you count browser cold start. Benchmark your target URLs; do not trust generic internet charts for capacity planning.
Q: Can I use both in one company?
A: Yes. Many teams keep Selenium Grid for legacy QA and use Playwright for new data collectors. Share schemas and compliance rules across both.
Q: Do I still need Beautiful Soup / lxml?
A: Often yes. Use the browser to obtain the HTML (or intercept JSON), then parse with a dedicated parser for speed and clearer unit tests.
Building Python scrapers or browser automation for logistics and ops? INDUS Web Agency designs production Playwright and Selenium workflows with clear compliance boundaries.
Top comments (0)