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.

Pydoll at a glance (read from PyPI and GitHub on 2 November 2026)
PackageMaintainerLatest releaseLicence and Python
pydoll-python, imported as pydollThalison Fernandes, thalissonvs on GitHub3.0.0, 29 September 2026MIT, 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

Shell · pip
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

Python · Pydoll 3.0.0, async
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.

Python · Pydoll 3.0.0, sync
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.

Python · find, query, type
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:

Python · proxy for the browser
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.

Python · proxy with a login
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.

Python · SOCKS5 with a login
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:

Python · get an hCaptcha token
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:

Python · fill the token, 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.

Solve hCaptcha in Python →

Last updated November 2026.

Frequently asked

What is Pydoll?
An open-source Python library for automating Chrome and Edge. It talks to the browser over the Chrome DevTools Protocol (CDP), with no WebDriver and no driver process, and it has both an async and a sync API. It is maintained by Thalison Fernandes under the autoscrape-labs organisation on GitHub and licensed MIT. Install it with pip install pydoll-python.
What is the latest version of Pydoll?
3.0.0, released on PyPI on 29 September 2026 (read on 2 November 2026). It added the synchronous API and the Playwright compatibility layer, and removed APIs that were deprecated in 2.x, such as browser.start(headless=True) and tab.execute_script(script, element). The project’s upgrade guide lists the changes.
Is Pydoll still maintained?
Yes, at a slower pace. 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; …” The last commit was on 29 September 2026, and 19 issues and 10 pull requests were open on 2 November 2026.
How do I run Pydoll headless?
Set 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?
Pydoll’s docs say to put the login in the flag, --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?
No. Its docs say plainly that its captcha feature “does not handle reCAPTCHA or hCaptcha”; the one challenge it automates is the Cloudflare Turnstile checkbox click. When a site shows hCaptcha, the form still needs a valid token in h-captcha-response, and Pydoll does not produce one. NoneCap returns that token.
Can Pydoll call the page’s hCaptcha callback?
Yes. 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.
Is it allowed to use Pydoll and a solver on a site?
That depends on the site and on what you do there. Automate where you are allowed to: your own sites, your tests, and services whose terms permit it. NoneCap’s terms rule out fraud, credential stuffing, spam and unauthorised access. This is not legal advice.

Start solving hCaptcha in minutes.

1,300 free credits on signup. Pay per solve, credits never expire, failed solves never charged.