fastcdp
fastcdp provides an async Python client for the Chrome DevTools Protocol (CDP) over WebSocket. It auto-discovers Chrome’s debug port, loads the full protocol schema from bundled JSON files, and exposes every CDP domain as a Python attribute with auto-generated signatures and docstrings — e.g. await cdp.page.navigate(url=...).
It can drive a Chrome it launches itself, one you started with a debug port, your everyday browser via Chrome’s built-in remote debugging, or — with the companion fastcdp-chrome extension — your everyday browser with no flags or popups at all.
It includes a Page class for tab-scoped operations, event subscription via cdp.on()/cdp.wait_event(), explicit navigation waits (goto, expect_navigation), content waits (wait_for_selector, wait_for), screenshot capture, and accessibility tree access. A cdp_search utility lets you search CDP commands by name or description. For use inside safepyrun sandboxes, cdp_yolo() registers all CDP classes.
Installation
Install latest from pypi
$ pip install fastcdp
How to use
from fastcdp import *
There are four ways to get connected (the fastcdp.skill module doc gives the full decision matrix):
cdp = await CDP.launch()— start a fresh, throwaway instance of your installed Chrome; zero setup.cdp = await CDP.connect()— attach to your everyday Chrome (146+) after enabling Allow remote debugging inchrome://inspect/#remote-debugging; Chrome gives you 60 seconds to approve each new client.cdp = await CDP.remote()— attach to a “debug Chrome” onremote’s default port 9223;fastcdp-setupcreates a launcher for exactly such a browser.cdp = await ExtCDP.listen()— wait for the fastcdp-chrome extension to dial in from your everyday browser: no flags, no popups.
This walkthrough uses connect:
Chrome 146+ has built-in remote debugging support. Navigate to chrome://inspect/#remote-debugging and enable “Allow remote debugging for this browser instance”:
The CDP class
Connect to Chrome (which will pop up a permissions window):
cdp = await CDP.connect()
Every CDP domain is available as an attribute with auto-generated signatures. You can search for commands with cdp_search:
cdp_search('screenshot')
"Emulation.setVisibleSize: Resizes the frame/viewport of the page. Note that this does not affect the frame's container\n(e.g. browser window). Can \nHeadlessExperimental.beginFrame: Sends a BeginFrame to the target and returns when the frame was completed. Optionally captures a\nscreenshot from the res\n evt Overlay.screenshotRequested: Fired when user asks to capture screenshot of some area on the page.\nPage.captureScreenshot: Capture page screenshot."
List open pages and attach to one:
ps = await cdp.pages
pg = ps[0]
pg['title']
'8. Database Transactions — PlanetScale'
tid = pg['targetId']
sid = await cdp.attach(tid)
await cdp.eval('document.title', sid)
'8. Database Transactions — PlanetScale'
The Page class wraps a tab with its own session, so you don’t need to pass sid everywhere:
page = await cdp.new_page()
await page.goto('https://httpbingo.org/forms/post')
goto waits for the document’s load event by default. Pass wait='idle' when initial network activity must also settle, or wait=None when the next application-specific content wait is a better definition of ready. You can wait_for any JS expression to become truthy and have its value returned:
await page.wait_for('document.title')
'6. httpbin.org/forms/post'
Take a screenshot of the page:
img = await page.screenshot()
Clean up when done:
await page.close()
await cdp.close()
See CDP docs for full details.
Page.new and Filling forms
Instead of CDP.connect, you can call Page.new with no params to automatically create a CDP object and attach it to a new page:
page = await Page.new()
await page.goto('https://httpbingo.org/forms/post')
For finding elements to interact with, use ax_tree. Pass frame_id= to read a child frame directly:
root = await page.ax_tree()
print(str(root)[:300])
- **RootWebArea** "6. httpbin.org/forms/post" `focusable=True` `focused=True` `url=https://httpbin.org/forms/post` [#2]
- **LabelText** "" [#24]
- **StaticText** "Customer name: " [#64]
- **InlineTextBox** "Customer name: "
- **textbox** "Customer name: " `focusable=True` `editable=p
find and find_id are used to identify elements in the tree:
nmid = root.find_id('textbox', 'Customer name')
nmid
4
Inspect a control’s HTML attributes with await page.attrs(nmid) or await page.attrs('[name=custname]'). Both return a dictionary. To inspect several elements, use await page.sel_attrs('input', 'name', 'type'). This returns one dictionary per match, with None for missing requested attributes. Omit the names to read all attributes.
You can use regular CDP methods, or one of the provided shortcuts:
await page.fill_text(nmid, 'Jeremy Howard')
await page.click(root.find_id('radio', 'Large'))
await page.js_node_run('this.value = "18:30"', root.find_id('InputTime', 'delivery time'))
{'type': 'undefined'}
click moves the real mouse before pressing and releasing. tap sends a trusted Chrome tap gesture without moving the mouse. dom_click calls the element’s JavaScript activation and does not produce trusted input. Use tap when mouse movement is unreliable or hover is undesirable.
click_and_wait uses click and requires a top-frame navigation. For another activation path, compose it with expect_navigation. For in-place UI updates, activate normally and wait for the resulting content.
await page.click_and_wait(root.find_id('button', 'Submit order'))
When using page.New(), close() also shuts down the CDP websocket.
await page.close()
To allow LLMs like solveit with safepyrun to access fastcdp, use:
cdp_yolo()
Then open a controlled page for it:
page = await Page.new()
Then use a prompt such as:
Try using python to go to
<url>using the existingpage, fill it out, read it to check it’s filled correctly, then submit it, and see what you get back. Don’t use find_id - you can get all the ids at once with ax_tree (don’t truncate the result of it). Don’t add extra waits etc - fastcdp handles it automatically. IDs can change so be sure to use the ax_tree IDs you read.
Release files for fastcdp 0.0.13
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| fastcdp-0.0.13.tar.gz | 291.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fastcdp-0.0.13-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 584.9 kB
Release files / fastcdp-0.0.13.tar.gz
| Download URL | fastcdp-0.0.13.tar.gz |
|---|---|
| Size | 291.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d2bd7c27ee4238ce43a04e9fbfba2f469b24359ee3c45108f09fa673528ff9c3
|
|
BLAKE2b-256 checksum How to use checksums |
982f83c143d3d208997b9589b17a9641e8f52d347e16147ebe05a56cdcb3cf73
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.15
|
Release files / fastcdp-0.0.13-py3-none-any.whl
| Download URL | fastcdp-0.0.13-py3-none-any.whl |
|---|---|
| Size | 293.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
011e9206604e98f9dc14ed915187ae9e37b9d5ea6e20b2b08a9207e3c5dfb171
|
|
BLAKE2b-256 checksum How to use checksums |
457c1f19c50b6109004251d2f2ff61a7f8dee562608a0915ed6499264d07ce5e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.15
|