Pyppeteer on its own is straightforward to detect. It leaks the WebDriver flag, uses the HeadlessChrome user agent string, returns an empty navigator.vendor, and fails other fingerprinting checks that anti-bot systems run on every request. Any serious bot detection will catch a bare Pyppeteer session before your scraping logic even runs.
pyppeteer_stealth is a plugin that patches those leaks. It applies a set of evasion techniques directly to the Pyppeteer page instance to make headless Chrome look more like a real browser session.
In this tutorial you will learn what pyppeteer_stealth patches, how to use it, how to fix the most common setup issue you will run into, and where its limits are against modern anti-bot systems.
What is pyppeteer_stealth? #
pyppeteer_stealth is the Python implementation of the Puppeteer Stealth plugin, built as an add-on for Pyppeteer, which is Python's unofficial port of Puppeteer. Where base Pyppeteer exposes clear automation signals, pyppeteer_stealth patches the most obvious ones.
Here is what it patches:
User Agent. Changes theHeadlessChrome
flag in the user agent to a standardChrome
string so the browser does not announce it is running headless.WebDriver. Setsnavigator.webdriver
tofalse
. This is the most commonly checked automation flag and one of the first things anti-bot systems look for.Chrome Runtime. Modifies the Chrome runtime object to make headless Chrome look like it is running in standard GUI mode.Hardware Concurrency. Overrides the CPU core count to match a realistic machine rather than the default value headless environments often return.Plugins. Populatesnavigator.plugins
with real browser plugin data. An empty plugin list is a strong automation signal.Vendor. Overridesnavigator.vendor
with a real vendor string. Headless Chrome returns an empty string here by default.WebGL. Spoofs GPU properties to return realistic hardware values rather than the generic software renderer headless environments use.Media Codecs. Replaces bot-like codec values with realistic MIME types that match a real browser installation.
How to use pyppeteer_stealth #
Install the libraries
pip3 install pyppeteer_stealth pyppeteer
Step 1: Run base Pyppeteer as a baseline
Before adding the stealth plugin, run a fingerprinting test to see what base Pyppeteer exposes. This gives you a clear before-and-after comparison.
import asyncio
from pyppeteer import launch
async def scraper():
browser = await launch(headless=True)
page = await browser.newPage()
await page.goto("https://bot.sannysoft.com/")
await page.screenshot({"path": "baseline.png"})
await browser.close()
asyncio.run(scraper())
The screenshot shows multiple red flags on the fingerprinting test. WebDriver exposed, HeadlessChrome in the user agent, empty plugins list. Base Pyppeteer fails a significant portion of the checks that anti-bot systems run.
Step 2: Add pyppeteer_stealth
Import the stealth
function and call it on the page instance after creating it but before navigating anywhere. The plugin patches the page context before any page code runs:
import asyncio
from pyppeteer import launch
from pyppeteer_stealth import stealth
async def scraper():
browser = await launch(headless=True)
page = await browser.newPage()
await stealth(page)
await page.goto("https://bot.sannysoft.com/")
await page.screenshot({"path": "stealth.png"})
await browser.close()
asyncio.run(scraper())
With the plugin applied, the fingerprinting test passes. The WebDriver flag is hidden, the user agent looks normal, plugins are populated, and the other patched properties return realistic values.
Two lines of change and the surface-level fingerprint looks clean.
Step 3: Scrape real data
Here is a complete example that uses pyppeteer_stealth to extract product data from an e-commerce page:
import asyncio
from pyppeteer import launch
from pyppeteer_stealth import stealth
async def scrape_products(url: str) -> list:
browser = await launch(headless=True)
page = await browser.newPage()
await stealth(page)
await page.goto(url, {"waitUntil": "networkidle0"})
products = await page.querySelectorAll(".product")
results = []
for product in products:
name = await product.querySelectorEval(
".product-name", "el => el.innerText"
)
price = await product.querySelectorEval(
".price", "el => el.innerText"
)
results.append({"name": name, "price": price})
await browser.close()
return results
data = asyncio.run(
scrape_products("https://www.scrapingcourse.com/ecommerce/")
)
print(data)
[
{"name": "Abominable Hoodie", "price": "$69.00"},
{"name": "Adrienne Trek Jacket", "price": "$57.00"},
{"name": "Artemis Running Short", "price": "$45.00"},
]
That works on an open page. Now test it against something with actual protection.
The common setup issue: Chromium not found #
When you run Pyppeteer for the first time, you may hit this error:
OSError: Chromium downloadable not found at
https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/1181205/chrome-win.zip
This happens because Pyppeteer targets a specific Chromium revision that is no longer available at that URL. The fix is to override the revision number before Pyppeteer tries to download it.
Find the chromium_down.py
file in your Pyppeteer installation. If you are using a virtual environment, it is at venv/Lib/site-packages/pyppeteer/chromium_down.py
. Add this line before the REVISION
variable:
import os
os.environ["PYPPETEER_CHROMIUM_REVISION"] = "1181217"
REVISION = os.environ.get("PYPPETEER_CHROMIUM_REVISION", __chromium_revision__)
You can also set the environment variable before running your script without touching the library files:
export PYPPETEER_CHROMIUM_REVISION=1181217
set PYPPETEER_CHROMIUM_REVISION=1181217
The limitations of pyppeteer_stealth #
pyppeteer_stealth passes fingerprinting tests. That is a meaningful improvement over base Pyppeteer. The limits become clear when you point it at a page with real anti-bot protection.
1. It fails against modern anti-bot systems
import asyncio
from pyppeteer import launch
from pyppeteer_stealth import stealth
async def scraper():
browser = await launch(headless=True)
page = await browser.newPage()
await stealth(page)
await page.goto("https://www.scrapingcourse.com/antibot-challenge")
await page.screenshot({"path": "blocked.png"})
await browser.close()
asyncio.run(scraper())
The screenshot shows the block page. pyppeteer_stealth patches surface fingerprints but it does not address the deeper JavaScript challenges, timing analysis, and behavioral checks that modern systems like Cloudflare run in the background.
2. It has not been updated since 2021
This is the most significant limitation. Anti-bot systems update frequently. A stealth library that has not changed in years is working against detection techniques from years ago. Many anti-bot vendors have specifically added detection for pyppeteer_stealth's patches since 2021 because its bypass mechanisms are public and well-documented in the open-source code.
3. Navigation patterns are still predictable
Even with fingerprints patched, the way Pyppeteer navigates pages, the timing between requests, the absence of natural reading s, and other behavioral signals can still look automated to systems that analyze traffic patterns rather than just browser properties.
4. No proxy infrastructure
pyppeteer_stealth has no built-in proxy rotation or geo-targeting. IP bans, rate limits, and geo-restrictions are entirely your problem to handle separately.
Going beyond pyppeteer_stealth #
When the plugin is not enough, you have two paths. You can add proxy rotation, switch to a more recently maintained stealth library, and keep tuning the setup manually. Or you can move the anti-bot handling out of your code entirely.
Spidra handles the full stack at the API level. Every request runs through a real browser with residential proxy rotation across 50 countries, CAPTCHA solving, and fingerprinting maintained against current detection techniques. It also replaces the HTML parsing step entirely: instead of returning raw HTML you still need to parse, it extracts exactly what you describe and returns clean structured JSON.
Here is the same anti-bot challenge page that blocked pyppeteer_stealth, using Spidra's Python SDK:
pip install spidra
python
from spidra import SpidraClient, ScrapeParams, ScrapeUrl
import os
spidra = SpidraClient(api_key=os.environ["SPIDRA_API_KEY"])
job = spidra.scrape.run_sync(ScrapeParams(
urls=[ScrapeUrl(url="https://www.scrapingcourse.com/antibot-challenge/")],
prompt="Extract the main heading",
use_proxy=True,
proxy_country="us",
))
print(job.result.content)
No browser to launch. No plugin to apply. No revision number to fix. The same request works on open pages and protected ones without any changes.
Here is the same e-commerce scraping task without any selectors or parsing:
job = spidra.scrape.run_sync(ScrapeParams(
urls=[ScrapeUrl(url="https://www.scrapingcourse.com/ecommerce/")],
prompt="Extract all product names and prices",
output="json",
))
print(job.result.content)
[
{"name": "Abominable Hoodie", "price": "$69.00"},
{"name": "Adrienne Trek Jacket", "price": "$57.00"},
{"name": "Artemis Running Short", "price": "$45.00"}
]
If you want a guaranteed output shape for downstream pipelines, add a schema:
job = spidra.scrape.run_sync(ScrapeParams(
urls=[ScrapeUrl(url="https://www.scrapingcourse.com/ecommerce/")],
prompt="Extract all products",
output="json",
schema={
"type": "array",
"items": {
"type": "object",
"required": ["name", "price"],
"properties": {
"name": {"type": "string"},
"price": {"type": "string"},
"image": {"type": ["string", "null"]},
}
}
}
))
Required fields always appear in every record, as null
if the page does not have that value.
pyppeteer_stealth vs. Spidra #
| pyppeteer_stealth | Spidra | |
|---|---|---|
| Fingerprint patching | Yes, 8 properties patched | Handled at infrastructure level |
| Last updated | 2021 | Actively maintained |
| Cloudflare bypass | Fails on JS challenges | Built in, automatic |
| DataDome / PerimeterX | Not reliable | Built in, automatic |
| Proxy rotation | Not included | Built in, 50 countries |
| Structured output | Raw HTML, you parse it | AI extraction, optional schema |
| Chromium setup issues | Yes, revision fix required | Not applicable |
| Maintenance as anti-bots evolve | Manual | Handled by Spidra |
| Language | Python | Python, Node.js, Go, PHP, Ruby, and 5 more |
| Best for | Light scraping, basic fingerprint patching | Protected sites, production pipelines |
Conclusion #
pyppeteer_stealth does what it says. It patches the most visible automation signals in Pyppeteer and a fingerprinting test looks much cleaner with it applied. For light scraping on sites without serious bot protection, it is a simple and low-effort improvement over base Pyppeteer.
The limitation is age. It has not been updated since 2021 and modern anti-bot systems have had years to study and specifically detect its patches. Against Cloudflare, DataDome, and similar systems it is not reliable, and the behavioral patterns Pyppeteer produces beyond the browser fingerprint are still detectable.
If you need to scrape sites that are actively trying to stop you, maintaining a patched Pyppeteer setup is ongoing work. Spidra handles the full anti-detection stack automatically so you can focus on the data rather than the browser setup.
Get started free at spidra.io. No credit card required.