Fully async Chrome DevTools Protocol over POSIX pipes with a high-level Browser/Tab/Elem API for Python 3.12+ on Linux.
Chrome automation using --remote-debugging-pipe (no websockets, no ports, just pipes) with bundled CDP protocol classes.
Inspired by playwright-python, python-cdp and nodriver.
Full documentation is auto-generated and published at: https://sohaib17.github.io/pypecdp/
- Fully Async: Built from ground up with asyncio for concurrent operations
- Fast: Direct pipe communication via file descriptors - no websockets, no network overhead
- Minimal dependencies: Only one dependency (
deprecated) - lightweight and easy to install - Secure: Browser only communicates over local pipes, no open ports accessible to other processes
- No zombies: No risk of orphaned Chrome processes if code crashes - automatic lifecycle management
- Linux focused: Leverages POSIX pipes and process management
pip install pypecdpInstall Chromium if needed:
# Ubuntu/Debian
sudo apt-get install chromium-browser
# Fedora
sudo dnf install chromium
# Arch
sudo pacman -S chromiumimport asyncio
from pypecdp import Browser
async def main():
# Launch browser
browser = await Browser.start(
chrome_path="chromium",
headless=True
)
# Navigate to a URL (creates/reuses a tab)
tab = await browser.navigate("https://example.com")
# Select and interact with elements
h1 = await tab.wait_for_elem("h1")
if h1:
text = await h1.text()
print(f"Page heading: {text}")
# Evaluate JavaScript
result = await tab.eval("document.title")
print(f"Title: {result.value}")
# Close browser
await browser.close()
asyncio.run(main())from pypecdp import Browser, Config
# Simple start
browser = await Browser.start(chrome_path="chromium", headless=True)
# Advanced configuration
config = Config(
chrome_path="/usr/bin/google-chrome",
user_data_dir="/tmp/chrome-profile",
clean_data_dir=False, # Preserve existing profile data
headless=True,
extra_args=["--no-sandbox", "--disable-gpu"],
env={"LANG": "en_US.UTF-8"}
)
browser = await Browser.start(config=config)
# Close browser
await browser.close()Note: By default, clean_data_dir=True which removes any existing user data directory before starting. Set it to False to preserve cookies, cache, and other browser state between runs.
# Finding and clicking elements
button = await tab.wait_for_elem("button.submit")
if button:
await button.click()
# Clicking elements that cause navigation
link = await tab.wait_for_elem('a[href="/next-page"]')
if link:
# click() returns the top-level Tab after navigation
current_tab = await link.click()
if current_tab:
# Wait for the new page to load
await current_tab.wait_for_event(cdp.page.LoadEventFired, timeout=10.0)
print(f"Navigated to: {current_tab.url}")pypecdp is designed to be easily extended through OOP inheritance:
from pypecdp import Browser, Tab, Elem
class MyElem(Elem):
async def click_and_wait(self, timeout=10.0):
"""Click and wait for page load."""
tab = await self.click()
if tab:
await tab.wait_for_event(cdp.page.LoadEventFired, timeout=timeout)
return tab
class MyTab(Tab):
elem_class = MyElem # Use custom Elem class
async def search(self, query):
"""Custom search method."""
search_box = await self.wait_for_elem("#search")
await search_box.type(query)
class MyBrowser(Browser):
tab_class = MyTab # Use custom Tab class
# Use your custom classes
browser = await MyBrowser.start()
tab = await browser.navigate("https://example.com")
# tab is now MyTab instance, elements are MyElem instancesSee example/customize_pypecdp.py for working example.
from pypecdp import cdp
# Tab-level events (requires domain enable!)
await tab.send(cdp.runtime.enable()) # Required for runtime events!
async def on_console(event):
print(f"Console {event.type_}: {event.args}")
tab.on(cdp.runtime.ConsoleAPICalled, on_console)
# Browser-level events
async def on_target_created(event):
info = event.target_info
print(f"Target created: {info.type_} - {info.url}")
browser.on(cdp.target.TargetCreated, on_target_created)pypecdp uses Python's standard logging module. Configure via environment variables:
# Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
export PYPECDP_LOGLEVEL=DEBUG
# Set custom logger name
export PYPECDP_LOGGER=myapp.browserOr configure the logger directly in Python:
from pypecdp import logger
import logging
# Set log level
logger.setLevel(logging.DEBUG)
# Add custom handler
handler = logging.FileHandler("pypecdp.log")
logger.addHandler(handler)try:
browser = await Browser.start()
tab = await browser.navigate("https://example.com")
# Your automation code
elem = await tab.wait_for_elem("button")
if elem:
await elem.click()
result = await tab.eval("document.title")
except ReferenceError as e:
# Element's tab is no longer available (closed/detached)
print(f"Target Error: {e}")
except RuntimeError as e:
# CDP protocol errors
print(f"CDP Error: {e}")
except ConnectionError as e:
# Connection lost
print(f"Connection Error: {e}")
except Exception as e:
# Other errors
print(f"Error: {e}")
finally:
# Always cleanup
await browser.close()- Python 3.12+
- Linux (uses POSIX pipes and
preexec_fn) - Chromium or Google Chrome
MIT License - See LICENSE file for details.
Contributions welcome! This project aims to provide a clean, type-safe interface to Chrome automation on Linux.