Pydoll: setup, proxies and hCaptcha in Python.
Pydoll is a Python library that drives Chrome over the DevTools Protocol, with no WebDriver. How to install it, async and sync quick starts, finding elements, headless mode, proxies with a login, and what to do when a site shows hCaptcha.
What Pydoll is
Pydoll’s README sums it up as “Browser automation for Python over raw CDP. No WebDriver, no driver process, sync or async, and your Playwright scripts run on it with one import.” CDP is the Chrome DevTools Protocol, the channel Chrome’s own developer tools use. Pydoll starts the browser and sends it commands over a WebSocket, so there is no ChromeDriver to download or keep in step with your Chrome version.
| Package | Maintainer | Latest release | Licence and Python |
|---|---|---|---|
pydoll-python, imported as pydoll | Thalison Fernandes, thalissonvs on GitHub | 3.0.0, 29 September 2026 | MIT, Python 3.10+ |
The project lives under the autoscrape-labs organisation on
GitHub; Thalison Fernandes wrote the large majority of its commits. It
drives the Chrome or Edge already installed on your machine. Besides
clicking and typing, it ships humanized mouse movement and typing, request
interception, HAR recording, typed data extraction into Pydantic models,
and a Playwright compatibility layer you switch on by changing one import.
The README also says that with no driver in between, “the usual
automation markers”, which it names as navigator.webdriver,
--enable-automation and Runtime.enable, “are
never there to begin with”, and that its apply_fingerprint()
passed CreepJS, SannySoft, BrowserScan and BrowserLeaks “without being
flagged”. Those are the project’s claims; we did not test them. In
our run, navigator.webdriver read False.
Install Pydoll
pip install pydoll-python
The PyPI package is pydoll-python; the module you import is
pydoll. It needs Python 3.10 or newer and, in the
README’s words, “Google Chrome or Microsoft Edge installed. No
WebDriver, no browser download, no Node.” Its dependencies are
aiohttp, aiofiles, pydantic,
websockets and typing_extensions.
Quick start in Python
import asyncio
from pydoll import Chrome, ChromiumOptions
async def main():
options = ChromiumOptions()
options.headless = True
async with Chrome(options=options) as browser:
tab = await browser.start()
await tab.go_to("https://your-site.example/")
print(await tab.title())
result = await tab.execute_script("return navigator.webdriver")
print(result["result"]["result"]["value"])
asyncio.run(main())
We ran this on 2 November 2026 with Pydoll 3.0.0, Python 3.13 and Chrome
156, headless. It printed the page title and False for
navigator.webdriver. execute_script() returns
the raw CDP response, so the value sits under
["result"]["result"]["value"].
A script that starts with return is wrapped in a function
for you.
Since 3.0 the same calls work without async: import from
pydoll.sync and drop the awaits.
from pydoll.sync import Chrome
with Chrome() as browser:
tab = browser.start()
tab.go_to("https://your-site.example/")
print(tab.title()) Finding elements
tab.find() takes the attributes you would use to describe an
element (id, tag_name, name,
class_name, text, or any other attribute with
underscores for hyphens) and matches the element that has all of them.
tab.query() takes a CSS selector or an XPath. By default both
look once and raise ElementNotFound if nothing matches; pass
timeout in seconds to wait for an element that loads late,
and find_all=True for every
match.
username = await tab.find(id="username")
await username.type_text("my-username", humanize=True)
password = await tab.find(tag_name="input", type="password")
await password.type_text("my-password")
submit = await tab.query("button[type=submit]")
await submit.click() humanize=True clicks into the
field and types with human-like timing. The README says
find() and query() also work inside cross-origin
iframes and shadow roots, including closed ones.
Headless mode
Set options.headless = True
before you create the browser, as in the quick start. Pydoll 3 removed
the 2.x form browser.start(headless=True).
In our run, headless Chrome 156 kept navigator.webdriver
false, but navigator.userAgent said
HeadlessChrome/156.0.0.0.
Pydoll’s own browser-options guide warns that “Headless is
detectable” and that “Setting a user agent does not hide it”; it points to
its fingerprint injection for that.
Use a proxy with Pydoll
Pydoll takes Chrome’s own proxy flag. The docs say HTTP, HTTPS and SOCKS5 URLs all work:
options = ChromiumOptions()
options.add_argument("--proxy-server=http://203.0.113.10:8080")
For a proxy that needs a login, the
proxy guide
says to put the username and password in the URL, and that Pydoll then
answers the proxy’s login challenge for you.
create_browser_context(proxy_server=...) takes the same URL
to give one browser context its own proxy.
options.add_argument("--proxy-server=http://user:pass@host:port")
We tested this against local test proxies. Without a login, all 3 of our
page loads through the proxy finished. With a login in the flag, results
were mixed and mostly bad: our first browser loaded both pages it tried,
then 2 of 8 browsers started at once loaded the page while the rest hung
until the timeout, and started one at a time only 1 of 6 loaded.
Through create_browser_context, the first tab of each
context loaded in all 8 runs, but a second tab in the same context hung
in all 4 we tried. That matches the open
issue #353,
“Proxy with authentication causes navigation timeout”, which traces it to
how Pydoll pauses requests while it waits for the login challenge.
What worked in all 3 of our loads was a SOCKS5 proxy through
SOCKS5Forwarder, a small local forwarder that ships with
Pydoll: Chrome connects to it without a login and it signs in to the
remote proxy. The docs offer it because Chrome ignores the login in a
socks5:// URL.
from pydoll.utils import SOCKS5Forwarder
forwarder = SOCKS5Forwarder(
remote_host="proxy.example.com",
remote_port=1080,
username="user",
password="pass",
local_port=1081,
)
async with forwarder:
options = ChromiumOptions()
options.add_argument("--proxy-server=socks5://127.0.0.1:1081")
async with Chrome(options=options) as browser:
tab = await browser.start()
await tab.go_to("https://your-site.example/") Is Pydoll still maintained?
Yes, at a calmer pace. Version 3.0.0 went to PyPI on 29 September 2026, the last of seven releases since May; the package has been on PyPI since March 2025, and the repository has about 7,100 stars. The README carries a note from the maintainer: “Pydoll is maintained by one person, and I’m a bit stretched at the moment, so new releases and replies to issues may take longer than usual. The project is not dead and it is not going anywhere; …” On 2 November 2026 the last commit was from 29 September, and 19 issues and 10 pull requests were open.
What Pydoll says about captchas
The README lists among Pydoll’s features that its humanized
interactions are “Often enough to pass behavioral challenges like
Cloudflare Turnstile or reCAPTCHA v3, depending on your browser and IP
reputation”, and that it “handles Cloudflare Turnstile”. The Turnstile
helper, tab.expect_cloudflare_turnstile(), finds the widget
and clicks its checkbox; the README adds “This isn’t a bypass.”
Those are the project’s claims; we did not test them.
hCaptcha is not on that list. The captcha page of its docs says the feature “does not solve image selection or puzzle challenges” and “does not handle reCAPTCHA or hCaptcha. Those are not supported by this feature.” The installed 3.0.0 package has no hCaptcha code.
How the browser looks is one of several things that decide whether
hCaptcha challenges you at all;
why am I getting hCaptcha
covers the others, starting with the reputation of your IP address. Once
hCaptcha is on the page, the site accepts the form only with a valid
token in the h-captcha-response
field, a P1_ string its server checks with hCaptcha.
Invisible hCaptcha needs that token too. Pydoll does not produce one.
Get an hCaptcha token into a Pydoll session
NoneCap is an hCaptcha solving API. Keep Pydoll for the rest of the run,
and when a page has hCaptcha, send its sitekey and
url to NoneCap, get a P1_ token back, put it in
the form and submit. The Python SDK
(pip install nonecap) has an async
client, AsyncNoneCap, that runs in Pydoll’s event loop,
and solve() waits for the result:
import os
from nonecap import AsyncNoneCap
from pydoll import Chrome
async def main():
async with Chrome() as browser:
tab = await browser.start()
await tab.go_to("https://your-site.example/signup")
widget = await tab.query("[data-sitekey]", timeout=10)
ua = await tab.execute_script("return navigator.userAgent")
async with AsyncNoneCap(api_key=os.environ["NONECAP_KEY"]) as nc:
solve = await nc.solve(
type="hcaptcha",
sitekey=widget.get_attribute("data-sitekey"),
url=await tab.current_url(),
user_agent=ua["result"]["result"]["value"],
)
await fill_and_submit(tab, solve.token)
Then put the token in both response fields, call the widget’s
callback if the page set one with data-callback, and submit:
import asyncio
import json
FILL = """(() => {
const token = %s;
document.querySelectorAll(
'textarea[name="h-captcha-response"], textarea[name="g-recaptcha-response"]'
).forEach((el) => (el.value = token));
const cb = document.querySelector("[data-callback]")?.getAttribute("data-callback");
if (cb && typeof window[cb] === "function") window[cb](token);
})()"""
async def fill_and_submit(tab, token):
await tab.execute_script(FILL % json.dumps(token))
button = await tab.query("button[type=submit]")
async with tab.expect_navigation():
await button.click()
asyncio.run(main()) execute_script() runs in the page’s own JavaScript
world, so the callback function the page defines is visible to it; some
tools run scripts in an isolated world by default, where it is not. We
tested both blocks against a local test page with an hCaptcha-style form
and a fake token, with the NoneCap call stubbed: both fields held the
token, the page’s callback received it, and the form posted it as
h-captcha-response and
g-recaptcha-response.
Sending the browser’s own user_agent is optional; some
sites need it for the token to be accepted. If the browser goes through a
proxy, you can pass the same one to NoneCap as proxy
(http://user:pass@host:port) so the
token and the form come from the same IP address. A solve that returns no
token is never charged.
Every field, status and error is in the API reference. NoneCap solves hCaptcha only; it does not help with reCAPTCHA, Turnstile or other CAPTCHAs.
Last updated November 2026.
Frequently asked
What is Pydoll?
pip install pydoll-python.What is the latest version of Pydoll?
browser.start(headless=True) and tab.execute_script(script, element). The project’s upgrade guide lists the changes.Is Pydoll still maintained?
How do I run Pydoll headless?
options.headless = True on a ChromiumOptions object before you create the browser; Pydoll 3 removed the old browser.start(headless=True). In our run, headless Chrome 156 kept navigator.webdriver false but sent HeadlessChrome in its user agent.How do I use a proxy with a username and password in Pydoll?
--proxy-server=http://user:pass@host:port, and Pydoll answers the proxy’s login challenge. In our test with Pydoll 3.0.0 most page loads through such a proxy never finished, which matches open issue #353. A SOCKS5 proxy through Pydoll’s SOCKS5Forwarder loaded in all 3 of our test loads.Does Pydoll solve hCaptcha?
h-captcha-response, and Pydoll does not produce one. NoneCap returns that token.Can Pydoll call the page’s hCaptcha callback?
tab.execute_script() runs in the page’s own JavaScript world, so functions the page defines, such as the one named in data-callback, are visible to it. In our test against a local page, the callback received the token we injected.