Skip to main content

Aselenium

Asynchronous browser automation for Python, with integrated WebDriver management.

Aselenium provides an asyncio-native interface to Chrome, Chromium, Microsoft Edge, Firefox, and Safari. Provision a compatible driver, configure a browser, and use an asynchronous session to navigate pages, interact with elements, run JavaScript, and capture results. Separate sessions can run concurrently where the browser backend permits it; Safari's current automation service is single-session.

Version 2.0.0: this README describes the current package. The 2.0 release removes legacy compatibility APIs and completes the driver-management and session-lifecycle modernization. Existing 1.x users should review the breaking-change guide before upgrading.

Version 2.0 no longer includes legacy compatibility paths. Chrome/Chromium provisioning requires version 115+, Firefox add-ons require WebExtension manifests, and browser commands use W3C WebDriver responses.

To try the package, follow installation, then choose the local HTML feature tour or the real-world Google demo. Both use real browsers: the local tour is headless by default, while the Google demo opens a visible window. Neither uses your personal browser profile.

Contents

Features

  • Async browser sessions: await browser operations without wrapping a synchronous Selenium client in application-managed threads.
  • Integrated driver provisioning: browser discovery, release-channel selection, exact versions, compatible versions, offline reuse, and Chrome for Testing.
  • Persistent local cache: SQLite-backed, platform-specific artifacts with executable integrity checks, atomic publication, persistent pins, and leases protecting active acquisitions from eviction.
  • Isolated configuration: options are captured when a context is acquired; configured profiles are physically cloned for each acquisition.
  • Stateful automation: elements, forms, cookies, windows, frames, shadow DOM, alerts, JavaScript, keyboard/pointer actions, screenshots, and PDF printing.
  • Bounded waits and owned cleanup: shared wait deadlines, serialized commands, session transactions, and cancellation-aware teardown.
  • Browser-specific tools: Chromium CDP, network emulation, permissions, logs, and casting; Firefox add-ons, contexts, and full-page screenshots; Safari's supported automation and permission commands.

Browser-specific features are not interchangeable. See the support boundaries before choosing a backend.

Installation

Requires Python 3.10 or newer. Runtime dependencies are aiohttp>=3.14.3, psutil>=5.8.0, and orjson>=3.11.6. No optional Feather-cache reader or pandas/pyarrow dependency remains. The usual browser must be installed separately, except when provisioning a Chrome for Testing browser/driver pair. Safari requires macOS and an existing Remote Automation setup.

After the GitHub 2.0.0 release has been published, install its attached wheel:

python -m pip install --upgrade https://github.com/AresJef/Aselenium/releases/download/v2.0.0/aselenium-2.0.0-py3-none-any.whl

When the optional PyPI promotion has also completed, the equivalent index installation is:

python -m pip install --upgrade aselenium==2.0.0

To develop from a source checkout, run the following from the repository root. If you already have a .venv, reuse it and skip the environment-creation command; you do not need to recreate it to update an editable installation.

On macOS/Linux:

python3 -m venv .venv
.venv/bin/python -m pip install -e .

On Windows, in PowerShell:

py -3 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -e .

Check the interpreter and imports before launching a browser (macOS/Linux):

.venv/bin/python -c "import sys, aselenium, orjson; print(sys.executable); print(aselenium.__file__); print(orjson.__file__)"

An editable install should resolve aselenium to this checkout's src/aselenium directory. Activation is optional when using the environment's Python executable explicitly. In an editor, select that same executable for Run/Debug; activating a terminal does not necessarily change the editor's choice. See dependency troubleshooting if an import fails.

python -m pip install aselenium without a version pin installs the newest distribution currently published. It does not obtain uncommitted changes from a working tree. Installing from GitHub likewise only obtains code that has actually been pushed. Confirm what the selected interpreter installed with:

.venv/bin/python -c "from importlib.metadata import version; print(version('aselenium'))"

Quick start

For ready-made command-line examples with reports and screenshots, go directly to the runnable demos. The two examples below are small launch scripts you can adapt for your own application. The Google example is the recommended real-world introduction; the local HTML example is the deterministic choice for learning element operations without consent pages, CAPTCHA, layout changes, or an Internet dependency.

Minimal local HTML example

Save this complete example as quickstart.py and run it in the environment where you installed the checkout. It uses an installed Chrome and a local data: page. It is headless, so no browser window is shown. Remove the --headless=new argument to make the browser visible. The provisioning call may contact driver vendors and download a driver. Browser acquisition afterward explicitly uses the cache offline.

import asyncio
from pathlib import Path
from urllib.parse import quote

from aselenium import Chrome


async def main():
    cache = Path("./browser-cache")
    cache.mkdir(parents=True, exist_ok=True)
    driver = Chrome(directory=cache)
    driver.options.add_arguments("--headless=new")
    driver.options.set_timeouts(implicit=0, pageLoad=20, script=5)
    driver.options.session_timeout = 30

    try:
        result = await driver.manager.install_result(
            version="build",
            policy="compatible-build",
            validate_compatibility=True,
        )
        print("Driver:", result.driver_version)
        print("Browser:", result.browser_version)

        async with driver.acquire(version="offline") as session:
            html = "<title>Aselenium</title><input id='name' aria-label='Name'>"
            await session.load("data:text/html;charset=utf-8," + quote(html))
            field = await session.wait_for(
                lambda: session.find_element("#name"), timeout=5
            )
            if field is None:
                raise RuntimeError("The input did not appear")
            await field.send("Hello from Aselenium")
            print(await session.title)
            print(await field.get_property("value"))
    finally:
        driver.options.close()


if __name__ == "__main__":
    asyncio.run(main())
.venv/bin/python quickstart.py

On later runs, change the installation policy to "offline" if vendor requests must be prohibited. The cache must contain a driver compatible with the current browser; a browser update may require reprovisioning. Offline provisioning is not a network sandbox for the browser itself.

In a notebook or another running event loop, use await main() instead of nesting asyncio.run().

Recommended: Google homepage

Save this as google_quickstart.py. It uses the same lifecycle but opens the real Google website in a visible Chrome window, prints the page URL/title, and keeps the window open for five seconds before cleanup. It does not submit a search, choose consent, or sign in. A minimal runnable copy is included as src/quick_start.py.

import asyncio
from pathlib import Path

from aselenium import Chrome


async def google_main():
    cache = Path("./browser-cache")
    cache.mkdir(parents=True, exist_ok=True)
    driver = Chrome(directory=cache)
    try:
        driver.options.set_timeouts(implicit=0, pageLoad=30, script=5)
        driver.options.session_timeout = 40

        result = await driver.manager.install_result(
            version="build",
            policy="compatible-build",
            validate_compatibility=True,
        )
        print("Driver:", result.driver_version)
        print("Browser:", result.browser_version)

        async with driver.acquire(version="offline") as session:
            await session.load("https://www.google.com/")
            print("URL:", await session.url)
            print("Title:", await session.title)
            await asyncio.sleep(5)
    finally:
        driver.options.close()


if __name__ == "__main__":
    asyncio.run(google_main())
.venv/bin/python google_quickstart.py

This minimal example only demonstrates navigation; a printed title does not prove that Google's normal homepage or search results are ready. For readiness checks, optional search, screenshots, and consent/challenge diagnostics, use src/demo_google.py. Google access requires an Internet connection even when driver provisioning uses policy="offline".

Usage guide

Most recipes below define functions to call from inside an active session, such as await fill_form(session, upload_path). They are not standalone launch scripts; use the quick-start lifecycle around them. Replace example selectors and URLs with those of your application. The demo fixtures provide a local form, iframe, shadow root, and other targets for experimentation.

An important convention: some properties perform browser requests and must be awaited (await session.title, await element.text). Local metadata and configuration are synchronous (session.id, driver.options, element.id).

Filesystem paths follow one package-wide contract:

Layer Path behavior
Public and high-level APIs Accept str, pathlib.Path, and compatible string-valued os.PathLike[str] objects. Byte-valued paths are rejected.
Core-function entry Convert and validate the supplied value once, expand ~, and anchor a relative path to the current working directory.
Internal workflow Retain and pass host-native pathlib.Path objects without converting them to strings and parsing them again. Path-valued public results also remain Path objects.
Text or portable path boundary Use str only for external interfaces that require text, such as process arguments, JSON/WebDriver payloads, URLs, SQLite/JSON records, and cache keys. Use PurePosixPath only for portable archive-member names.

You do not need to call str() or resolve() before passing a path. Keeping the native Path after the boundary avoids platform-specific reparsing differences, especially on Windows. Filesystem operations can still perform scoped canonicalization when security or ownership checks require it; that is validation of an established Path, not a return to string-based path handling.

Driver management

Each facade owns a manager, available as driver.manager. You can also use a standalone manager without creating a browser session:

from pathlib import Path
from aselenium import ChromeDriverManager


async def provision_chrome(cache_directory: str | Path, *, offline: bool = False):
    directory = Path(cache_directory).expanduser()
    directory.mkdir(parents=True, exist_ok=True)
    manager = ChromeDriverManager(directory=directory)
    result = await manager.install_result(
        version="build",
        policy="offline" if offline else "compatible-build",
        validate_compatibility=True,
    )
    return result

The standalone classes are ChromeDriverManager, ChromiumDriverManager, EdgeDriverManager, FirefoxDriverManager, and SafariDriverManager.

await manager.install(...) returns an absolute pathlib.Path to the selected driver executable. await manager.install_result(...) returns an immutable snapshot with:

Field Meaning
request Requested product, selector, channel, parsed Path executable overrides, platform, policy, and compatibility-validation flag.
driver_location Absolute Path to the selected driver executable.
driver_version Selected driver version string, or None only when a custom manager cannot report it.
browser_location Absolute browser-executable Path, or None when a custom manager does not select a browser.
browser_version Selected browser version string, or None when it cannot be reported.
channel The resolved browser channel.

Use the returned result when coordinating concurrent calls. Mutable manager properties and last_result describe the last successful installation, not the installation belonging to a particular caller.

Resolution policies and selectors

Pass a policy to install_result(), not to driver.acquire():

Policy Intended use
compatible-build Match the installed Chromium-family browser's build.
compatible-major Match a Chromium-family major version; session startup still validates compatibility.
latest-compatible Refresh vendor resolution for a compatible candidate rather than immediately returning a cache hit. It does not upgrade an installed browser.
cached-compatible Prefer a compatible cached driver; provision if one is missing.
offline Use cached artifacts without vendor requests; fail on a miss.
exact Resolve the complete numeric driver version rather than substitute a newer patch.

The familiar selectors remain available through install() and acquire():

Browser family Selectors
Chrome / Chromium / Edge "build" (default), "major", "patch", "latest", "cached", "offline"; a numeric major, three-part build, or four-part exact version. "patch" targets the detected browser's complete version, not any latest patch.
Firefox "latest" (default), "auto" / "cached" (cache first), "offline", or a full three-part Gecko version. Chromium build/major policies do not describe Gecko compatibility.
Safari channel="stable" or channel="dev"; optional driver and binary paths. No downloadable driver-version selector or cache.

Two-component Chromium selectors are rejected. Numeric selectors must not have decorations such as a v prefix. A complete Chromium pin has four components; a Gecko pin has three. The version must actually exist for the target platform.

from aselenium import Chrome


async def install_exact(driver: Chrome, version: str):
    # Supply a real four-part driver version compatible with your Chrome.
    result = await driver.manager.install_result(
        version=version, policy="exact", validate_compatibility=True
    )
    # Exact installation already pins the artifact against eviction.
    # Explicit pin/unpin is also available:
    if result.driver_version is None:
        raise RuntimeError("The driver manager did not report an installed version")
    await driver.manager.pin(result.driver_version, pinned=True)
    return result


async def allow_eviction(driver: Chrome, version: str):
    await driver.manager.pin(version, pinned=False)

Standalone provisioning defaults to validate_compatibility=False, allowing prewarming for another browser version. Set it to True when checking a pair you intend to use locally. Session acquisition always validates the pair.

For offline startup, use driver.acquire(version="offline"). Supplying an exact numeric version to acquire() instead can download it if absent. A preceding exact installation does not force a later "offline" acquisition to select that same version if several compatible cached versions exist; inspect the actual session version and keep version-selection requirements explicit.

Chrome for Testing

Chrome can provision a matching Chrome for Testing (CfT) browser and driver:

from aselenium import Chrome


async def provision_cft(driver: Chrome, version: str, *, offline: bool = False):
    # Supply a published, complete four-part CfT version.
    result = await driver.manager.install_result(
        version=version,
        channel="cft",
        policy="offline" if offline else "exact",
        validate_compatibility=True,
    )
    return result

driver.acquire(version=version, channel="cft") starts that CfT pair and can provision missing assets. The CfT route requires a numeric selector; it does not accept version="offline". For the offline-compatible-binary workflow used by the local demo, see the CfT instructions.

CfT availability is verified against the version manifest. OS/architecture support depends on the assets actually published for that version; unsupported ARM combinations are not silently treated as x64. Exact CfT installation pins both artifacts. Use manager.pin(version, artifact="binary", pinned=False) to unpin its browser separately from its driver.

Cache directory and limits

Without directory, downloadable managers use ~/.aselenium. With an explicit existing cache parent, the v2 index is <directory>/.aselenium/v2/index.sqlite3. Create the parent before constructing a facade/manager. Use a private local filesystem, not a multi-host shared cache.

max_cache_size limits retained cached versions, not bytes. Pins and active session leases can keep the cache above that soft limit. Raw executable paths used outside Aselenium session contexts are not automatically leased; pin an artifact before relying on it under eviction pressure.

Executable hashes detect local executable changes; they are not vendor signatures or validation of every file in a browser bundle. Corrupt entries and unsupported database schemas are reported and preserved, not silently erased.

Choosing a browser

Import the facade you need from aselenium. Browser selection belongs to the facade and its acquire() arguments, not to a generic Selenium driver object.

Facade Acquisition example Configuration notes
Chrome() driver.acquire("build", channel="stable", binary=path) Stable, beta, dev, and numeric CfT route. Headless: --headless=new.
Chromium() driver.acquire("build", binary=path) No channel parameter. Headless: --headless=new.
Edge() driver.acquire("build", channel="stable", binary=path) Stable, beta, dev; no CfT channel. Headless: --headless=new.
Firefox() driver.acquire("auto", binary=path) Gecko compatibility resolution; no channel parameter. Headless: -headless.
Safari() driver.acquire(channel="stable", binary=path) System Safari driver, macOS, headed automation. dev selects Technology Preview.

Omit binary or pass None for discovery. An explicit path identifies the actual executable. For example, a macOS Chrome path is /Applications/Google Chrome.app/Contents/MacOS/Google Chrome, not the app bundle directory. On Windows, use the browser's .exe path.

Chrome/Chromium/Edge/Firefox constructors accept directory, max_cache_size, request_timeout, download_timeout, a provisioning proxy, and service_timeout. Safari does not accept download/cache constructor settings; do not copy those keyword arguments into Safari().

Firefox additionally accepts profile_root. This is the parent in which GeckoDriver creates temporary browser profiles; it is not the same setting as driver.options.set_profile(), which selects profile content to clone.

Options and proxies

Configure driver.options before calling acquire(). Importing a separate options class is optional.

from aselenium import Chrome


def configure_chrome(driver: Chrome):
    options = driver.options
    options.add_arguments("--headless=new", "--window-size=1200,900")
    options.accept_insecure_certs = False
    options.page_load_strategy = "normal"  # Also: "eager" or "none".
    options.unhandled_prompt_behavior = "ignore"  # Handle prompts explicitly.
    options.strict_file_interactability = True
    options.session_timeout = 30
    options.set_timeouts(implicit=0, pageLoad=20, script=5)
    options.set_preferences(
        **{
            "credentials_enable_service": False,
            "profile.password_manager_enabled": False,
        }
    )
    options.add_experimental_options(excludeSwitches=["enable-logging"])

Arguments, preferences, and experimental options are browser-specific. Do not assume Chromium keys have meaning in Firefox or Safari. For trusted Chromium extensions, options.add_extensions(*paths) accepts packaged extension paths; Firefox's session-level add-on APIs are described below.

Capability/preference dictionaries and argument lists returned by accessors are defensive copies. Changing driver.options.capabilities["pageLoadStrategy"] does not reconfigure the driver. Use properties or methods such as set_capability(), set_preferences(), and add_arguments(). An invalid argument or extension batch does not partially apply earlier items in that batch.

There are two distinct proxy settings:

  • The facade/manager constructor's proxy configures driver-provisioning HTTP requests. It does not configure browser page traffic.
  • driver.options.proxy configures the browser's proxy capability, where supported. It does not control driver downloads.
from aselenium import Chrome, Proxy


def configure_browser_proxy(driver: Chrome, proxy_url: str):
    # Supply a real reachable URL, for example http://proxy.example:8080.
    driver.options.proxy = Proxy(
        http_proxy=proxy_url,
        https_proxy=proxy_url,
        no_proxy=["localhost", "127.0.0.1"],
    )

Construct Proxy with keyword arguments and explicit URL schemes; the old FTP proxy option and positional constructor arguments have been removed. Proxy bypass entries serialize to the WebDriver noProxy array. Credentials are redacted from proxy representations, but avoid printing full capability dictionaries or storing credentials in source files. Actual proxy authentication support varies by browser. Safari's facade does not support custom proxy configuration. Provisioning keeps TLS verification enabled and does not implicitly use environment-proxy discovery.

Profiles and configuration isolation

set_profile() makes a temporary copy instead of launching against the original profile. Chromium-family browsers take a user-data directory and profile folder; Firefox takes a profile directory directly. Safari does not expose this feature. The Chromium profile folder must be a portable single child-directory name, not a path: separators, absolute or drive syntax, ./.., and reserved names are rejected.

from aselenium import Chrome


async def use_dedicated_profile(profile_directory: str):
    driver = Chrome()
    driver.options.add_arguments("--headless=new")
    try:
        # profile_directory must exist and contain a "Default" profile folder.
        driver.options.set_profile(profile_directory, "Default")
        async with driver.acquire("build") as session:
            await session.load("data:text/html,<title>Cloned profile</title>")
            return await session.title
    finally:
        # Release the facade's template as well as session-owned snapshots.
        driver.options.close()

For Firefox, the corresponding call is driver.options.set_profile(profile_directory).

Prefer a small, dedicated automation profile. Copying an actively modified personal profile does not produce a guaranteed consistent snapshot and may copy cookies or other sensitive data. The demo's --profile-demo uses an empty temporary source instead.

driver.acquire() captures configuration when called, before it is awaited. Later option changes only affect later contexts. Configured profiles receive separate physical clones, so closing one acquisition does not remove another's profile. The synchronous acquisition API still copies an explicitly configured profile synchronously; large profiles can block the event loop during this step.

Do not share a manually supplied --user-data-dir between running sessions. Same-process active sharing is rejected; cross-process sharing is unsupported.

Firefox Snap/Flatpak profile root

Container-packaged Firefox can have a different temporary-filesystem view from the host GeckoDriver. On affected Linux systems, pass an existing, non-hidden directory under your home directory that both processes can read and write:

from pathlib import Path

from aselenium import Firefox


async def open_google_with_containerized_firefox():
    profile_root = Path.home() / "aselenium-firefox-profiles"
    profile_root.mkdir(parents=True, exist_ok=True)

    driver = Firefox(profile_root=profile_root)
    driver.options.add_arguments("-headless")
    try:
        async with driver.acquire("auto") as session:
            await session.load("https://www.google.com/")
            return await session.title
    finally:
        driver.options.close()

The public argument accepts str, pathlib.Path, or a string-valued os.PathLike. The reusable Firefox facade parses and validates it once, then retains the resulting Path across acquisitions. Each service launch rechecks that the retained directory still exists without converting it back to text. The directory must already exist and GeckoDriver must be 0.32.0 or newer. It remains caller-owned and is never deleted by Aselenium. Do not also pass a raw --profile-root service argument. See Mozilla's container-package guidance for the underlying GeckoDriver/Firefox filesystem constraint.

Session lifecycle

Use async with driver.acquire(...) as session for ordinary work. It owns the driver service, HTTP client, session state, cache leases, and profile snapshot.

For explicit lifecycle control:

from aselenium import Chrome


async def managed_session(driver: Chrome):
    context = driver.acquire("offline")  # Requires a compatible cached driver.
    try:
        session = await context.start()
        await session.load("data:text/html,<title>Managed session</title>")
        return await session.title
    finally:
        await context.quit()

Repeated start() calls share an already running context. A closed context is single-use: create a new acquisition instead of restarting it. quit() is idempotent after successful cleanup and protects owned teardown from cancellation. If cleanup fails, the context retains resource handles for an explicit quit() retry; do not overwrite it with a restart. Keep the original exception visible when deciding how to report a cleanup failure.

Timeouts and explicit waits

These settings solve different problems:

Setting Unit and scope
request_timeout / download_timeout Seconds; provisioning metadata requests and downloads.
service_timeout Seconds; driver-service startup/shutdown operations.
options.session_timeout Seconds; a WebDriver command budget including ownership/connection queueing and the HTTP request.
set_timeouts(implicit=...) Seconds; element-lookup waiting. Zero avoids combining a long implicit wait with explicit polling.
set_timeouts(pageLoad=...) Seconds; the browser's page-load timeout.
set_timeouts(script=...) Seconds; the browser's asynchronous-script timeout.
wait_for(..., timeout=...) and wait_until_* Seconds; a total polling budget shared with nested waits and their commands.

Manager, service, and command budgets must be finite and positive. Polling permits zero: timeout=0 or None makes one immediate observation, not an infinite wait. Timeouts.dict is a WebDriver wire representation in milliseconds; the ordinary setter arguments and attributes above use seconds. Explicit *_ms attributes also exist on Timeouts.

from aselenium import Session


async def wait_for_input(session: Session):
    await session.set_timeouts(implicit=0, pageLoad=20, script=5)
    field = await session.wait_for(lambda: session.find_element("#name"), timeout=5)
    if not field:
        raise TimeoutError("#name did not appear within five seconds")
    if not await field.wait_until("enabled", timeout=2):
        raise TimeoutError("#name did not become enabled")
    return field

wait_for() takes an asynchronous, no-argument predicate and returns its first truthy value, or a falsey value when its deadline expires. Other exceptions propagate. It does not automatically ignore every transient element exception; re-locate elements in a predicate when the page can replace them.

Useful built-ins include wait_until_title(), wait_until_url(), wait_until_element(), and wait_until_elements(). Title/URL conditions are equals, contains, startswith, and endswith. Session element conditions include exist, gone, in_viewport, unobscured, enabled, and selected. For example, await session.wait_until_element("unobscured", "#name", timeout=5) waits for the element's center-point hit test to succeed. The old visible and viewable properties and wait conditions have been removed, not aliased. Boolean wait helpers return False on an unmet deadline; check their results. await session.reset_timeouts() restores the acquisition's original settings.

Navigation and page information

from aselenium import Session


async def inspect_google_homepage(session: Session):
    await session.load("https://www.google.com/", timeout=30)
    return {
        "url": await session.url,
        "title": await session.title,
        "source": await session.page_source,
        "page_width": await session.page_width,
        "page_height": await session.page_height,
        "viewport": await session.viewport,
        "window_rect": await session.window_rect,
    }

Google may redirect to a regional or consent page, so this example deliberately reports the resulting URL instead of asserting that it is byte-for-byte equal to the requested URL. For a controlled application URL, use wait_until_url("equals", expected_url, timeout=5); for redirecting applications, choose contains, startswith, or endswith according to the routing contract. The guarded Google demo additionally checks the origin, consent state and challenge pages before attempting interaction.

await session.refresh(), await session.backward(), and await session.forward() operate on the active window. Navigation can redirect; choose a URL condition appropriate to the application.

load() and refresh() have an explicit retry option for native WebDriver page-load timeouts. They do not retry command transport timeouts automatically. Only enable retries when repeating the navigation is safe for your application.

Finding and interacting with elements

Session and element-scoped lookup support CSS selectors (by="css", the default) and XPath (by="xpath"). Use relative XPath such as .//input when searching within an element.

Lookup Missing-result behavior
find_element(selector) Returns Element or None.
find_elements(selector) Returns a list, empty on a miss.
find_1st_element(selector_a, selector_b, ...) Returns the first found element or None; every locator is checked once even with zero implicit wait.
element_exists(selector) / elements_exist(...) Immediate boolean existence checks, ignoring implicit wait.
from pathlib import Path
from aselenium import Session


async def fill_form(session: Session, upload_path: str | Path):
    # This recipe uses the form IDs in src/demo_assets/index.html.
    field = await session.find_1st_element("#name", "input[name='name']")
    if field is None:
        raise LookupError("Name input not found")
    await field.clear()
    await field.send("Aselenium")  # Element.send(), not send_keys().

    checkbox = await session.find_element("#subscribe")
    if checkbox is not None and not await checkbox.selected:
        await checkbox.click()

    upload = await session.find_element("#upload")
    if upload is None:
        raise LookupError("File input not found")
    await upload.upload(upload_path)
    await field.submit()  # Awaited submission of the enclosing form.

upload() targets an <input type="file">; it is not automation of a native file picker. Selecting a file makes it available to the page, so only upload files to origins you trust. submit() and button clicks may trigger application side effects; wait for an application-specific result rather than assuming immediate completion. Re-find stale elements after navigation or DOM replacement.

The form recipe intentionally remains local: Google selectors, consent flows and search-result behavior can change and automated search may encounter a challenge. For a real-world, defensive lookup that checks multiple Google search-field layouts, enabled state, obstruction, redirects and CAPTCHA without bypassing them, use src/demo_google.py.

DOM properties and visibility

from aselenium import Element


async def inspect_element(element: Element):
    return {
        "rendered_text": await element.text,
        "dom_text": await element.dom_text,
        "value": await element.get_property("value"),
        "placeholder_attribute": await element.get_attribute_dom("placeholder"),
        "css_color": await element.get_property_css("color"),
        "enabled": await element.enabled,
        "selected": await element.selected,
        "in_viewport": await element.in_viewport,
        "unobscured": await element.unobscured,
        "aria_role": await element.aria_role,
        "aria_label": await element.aria_label,
    }

text is WebDriver's rendered text; dom_text reads DOM textContent. get_attribute_dom() reads a DOM attribute, while get_property() reads a JavaScript property. For example, an input's current value can differ from its original value attribute.

in_viewport requires a nonempty rectangle intersecting the viewport. unobscured uses center-point hit testing, including open shadow-root traversal. These are different observations, not promises that a later click will succeed. Geometry alone does not imply CSS visibility: a visibility:hidden element can still have a nonempty rectangle. Use get_property_css() when checking a specific style condition. The hybrid get_attribute() helper is removed; choose get_attribute_dom() or get_property() explicitly. Unsupported inspection commands may return None.

Windows and tabs

Window objects have synchronous name and handle properties. Window lists and focus are asynchronous: await session.windows, await session.active_window.

from aselenium import Session


async def inspect_another_tab(session: Session, url: str):
    async with session.transaction():
        original = await session.active_window
        await session.new_window("details", win_type="tab", switch=True)
        try:
            await session.load(url)
            return await session.title
        finally:
            await session.close_window(switch_to=original)

Use win_type="window" for a separate window. switch_window() accepts a name, handle, or Window. Names must be nonempty and unique among open windows. close_window() closes the active window; switch_to names the remaining window to focus, not the window to close.

set_window_rect(width=..., height=..., x=..., y=...), maximize_window(), minimize_window(), and fullscreen_window() control window geometry where the browser supports it. Commands on one connection are serialized, but separate multi-command tasks can still interleave. A transaction() protects a related sequence such as switching tabs and reading from the selected tab. Transaction ownership follows the inherited async context. Child tasks created inside an explicit transaction share that ownership, so await dependent operations sequentially there rather than running them concurrently with asyncio.gather(). Waiting to enter the transaction is bounded by the enclosing command/wait deadline, or options.session_timeout if there is none. This is an admission budget, not a total deadline for the context body, and it provides no rollback. If admission expires, no command from that waiting context has been sent.

Frames and shadow DOM

Switch frames before locating elements inside them, then restore the top-level document in a finally block. The current Safari facade disables frame switching.

from aselenium import Session


async def read_frame(session: Session):
    try:
        if not await session.switch_frame("#frame", timeout=5):
            raise TimeoutError("Frame not available")
        element = await session.find_element("#frame-text")
        return None if element is None else await element.text
    finally:
        await session.default_frame()


async def read_shadow_content(session: Session):
    host = await session.find_element("#host")
    if host is None:
        return None
    shadow = await host.shadow  # The property is named shadow, not shadow_root.
    if shadow is None:
        return None
    element = await shadow.find_element("#shadow-text")
    return None if element is None else await element.text

Frames can also be selected by an Element or by index using by="index". parent_frame() moves up one level; default_frame() returns to the document root. Selector-based frame waits re-resolve the element on each attempt. Shadow lookup uses CSS selectors; a Shadow is a separate search context, not a normal DOM element. Do not assume closed shadow roots are accessible.

Alerts and prompts

Configure unhandled_prompt_behavior="ignore" before acquisition if your code will handle prompts explicitly. Use an application action to open the prompt, or schedule a local one after the script returns:

from aselenium import Session


async def answer_prompt(session: Session):
    await session.execute_script("setTimeout(() => prompt('Your name?', ''), 100)")
    alert = await session.get_alert(timeout=5)
    if alert is None:
        raise TimeoutError("Prompt did not appear")
    try:
        print(await alert.text)
        await alert.send("Aselenium")  # Text input applies to a prompt.
    finally:
        await alert.accept()

Use dismiss() to cancel an alert/confirmation/prompt. Handle modal prompts before issuing unrelated page commands, which may be rejected while the prompt is open.

Cookies

Navigate to the intended HTTP(S) origin before manipulating its cookies. data: pages, including the quick start's page, are not suitable cookie origins.

from aselenium import Session


async def round_trip_cookie(session: Session):
    # Assumes the session is already on your HTTP(S) page.
    await session.add_cookie(
        {
            "name": "demo-preference",
            "value": "compact",
            "path": "/",
            "sameSite": "Lax",
        }
    )
    try:
        cookie = await session.get_cookie("demo-preference")
        return None if cookie is None else cookie["value"]
    finally:
        await session.delete_cookie("demo-preference")

Cookie results are Cookie objects with dictionary-style field access and a .dict snapshot, not a .value property. await session.cookies retrieves all cookies for the active page context; await session.delete_cookies() deletes them. Treat cookie values as secrets and respect domain, secure, expiry, and same-site constraints.

JavaScript

execute_script() returns the script's result. Values and Element arguments are serialized for WebDriver. Do not interpolate user input directly into script source when it can be passed as an argument.

from aselenium import Session


async def javascript_examples(session: Session):
    title = await session.execute_script("return document.title")
    field = await session.find_element("#name")
    if field is not None:
        await session.execute_script("arguments[0].focus()", field)

    cached = session.cache_script("sum", "return arguments[0] + arguments[1]", 2, 3)
    try:
        total = await session.execute_script(cached)
        overridden = await session.execute_script("sum", 10, 20)
    finally:
        session.remove_script("sum")

    result = await session.execute_async_script("""
        const done = arguments[arguments.length - 1];
        setTimeout(() => done({ready: true}), 25);
    """)
    return title, total, overridden, result

cache_script() and remove_script() are synchronous local operations. A cached script can be addressed by its returned JavaScript object or name; supplied execution arguments override its cached arguments. execute_async_script() uses WebDriver's callback convention, bounded by the script timeout.

A returned dictionary containing error or message is ordinary application data when the WebDriver response itself succeeded. Raw element-reference objects returned inside script results are not automatically turned into Element instances; use the element lookup APIs when you need element wrappers.

Keyboard and pointer actions

Use Element.send() for simple input. For pointer movement, clicks, keyboard combinations, drag operations, or wheel actions, construct an Actions chain. Building the chain is synchronous; perform() and reset() are asynchronous.

from aselenium import Session


async def type_with_actions(session: Session):
    field = await session.find_element("#name")
    if field is None:
        raise LookupError("Name input not found")
    await field.clear()
    chain = session.actions()
    try:
        await chain.move_to(field).click().send_keys("first").perform()
        await chain.send_keys("-second").perform()
        return await field.get_property("value")
    finally:
        await chain.reset()

perform() detaches the pending batch before awaiting the browser. Later input queued through the builder belongs to a new batch and is retained even if the earlier dispatch fails or is cancelled. A dispatched batch is never automatically replayed. Overlapping calls do not duplicate the batch, but dependent interactions should still be awaited sequentially. reset() clears the then-pending local batch and requests release of remote input state. Input added after reset starts is kept for a later perform(). send_key_combo() accepts KeyboardKeys values for modifiers; choose the appropriate Control/Command modifier for the operating system. MouseButtons provides button constants. Durations must be finite and nonnegative. Optional pause= values on elements, alerts, and scrolling, and explicit_wait= on perform(), are validated before sending a browser mutation. Booleans, negative values, and non-finite numbers are rejected; None means no delay. The current Safari facade disables action chains; Firefox support should be validated for the operations and browser versions your application needs.

Scrolling

from aselenium import Session


async def reach_section(session: Session, selector: str):
    await session.scroll_by(height=300)
    if not await session.scroll_into_view(selector, timeout=5):
        raise TimeoutError("Scroll target not available")
    # Read or interact with the target here.
    await session.scroll_to(x=0, y=0)

An Element also has scroll_into_view(). Helpers scroll_to_top(), scroll_to_bottom(), scroll_to_left(), and scroll_to_right() perform stepped scrolling. They stop on repeated lack of progress or bounded iteration/deadline limits; they do not promise to exhaust an endlessly growing feed. For infinite scrolling, define an application-specific item or time limit.

Screenshots and PDF output

import asyncio
from pathlib import Path
from aselenium import Session


async def capture_page(
    session: Session, output_directory: str | Path, *, pdf: bool = False
):
    output = Path(output_directory).expanduser()
    await asyncio.to_thread(output.mkdir, parents=True, exist_ok=True)
    if not await session.save_screenshot(output / "page.png"):
        raise RuntimeError("Screenshot was not saved")
    if pdf and not await session.save_page(output / "page.pdf", background=True):
        raise RuntimeError("PDF was not saved or printing is unsupported")

take_screenshot() returns PNG bytes; print_page() returns PDF data where supported. Element.take_screenshot() / save_screenshot() capture an individual element. save_page() also accepts orientation, scale, dimensions, margins, shrink-to-fit, and page ranges. A parent directory must exist. Relative filenames are supported; PNG/PDF output is published atomically using off-loop file work. Successful saves can replace an existing file, so choose unique names when preserving prior captures matters. Check boolean/optional results.

Chromium printing is demonstrated and validated in headless mode; Safari's facade disables printing. Firefox adds take_full_screenshot() and save_full_screenshot() for full-page capture. Captures can contain sensitive page content; handle their storage and sharing accordingly.

Concurrent sessions and cancellation

Separate sessions are the simplest concurrency boundary. A single connection serializes its commands; launching many coroutines against one session does not give independent tab, frame, cookie, or focus state.

This Python 3.10-compatible recipe bounds active sessions and owns all its tasks. Provision the shared cache first and pass a finite batch of URLs.

import asyncio
from aselenium import Chrome


async def collect_titles(driver: Chrome, urls: list[str], parallelism: int = 3):
    if parallelism < 1:
        raise ValueError("parallelism must be positive")
    slots = asyncio.Semaphore(parallelism)

    async def worker(url: str):
        async with slots:
            async with driver.acquire("offline") as session:
                await session.load(url)
                return await session.title

    tasks = [asyncio.create_task(worker(url)) for url in urls]
    try:
        return await asyncio.gather(*tasks)
    finally:
        for task in tasks:
            if not task.done():
                task.cancel()
        cleanup = asyncio.gather(*tasks, return_exceptions=True)
        while not cleanup.done():
            try:
                await asyncio.shield(cleanup)
            except asyncio.CancelledError:
                # Repeated cancellation must not abandon owned workers.
                continue
        cleanup.result()

Configure the facade before launching workers. Each acquire() snapshots options; use independent facades if workers require different configurations. This pattern is not intended for Safari's single-session automation service.

For a work deadline, wrap an owned coroutine in asyncio.wait_for(). Cancellation must unwind the session context; do not simply cancel a task and discard it. Cleanup may exceed the work deadline while owned subprocess/filesystem work finishes. Python cannot forcibly interrupt a blocking kernel operation just because it was moved to a worker thread. See the runnable demo's cancellation chapter for cancellation followed by a fresh acquisition.

Browser-specific features

Chromium: CDP, network conditions, permissions, and logs

These APIs belong to ChromeSession, ChromiumSession, and EdgeSession. They are not portable Firefox/Safari methods.

from aselenium import ChromeSession, ChromiumSession, EdgeSession


async def chromium_diagnostics(session: ChromeSession | ChromiumSession | EdgeSession):
    command = session.cache_cdp_cmd("browser-version", "Browser.getVersion")
    version = await session.execute_cdp_cmd(command)

    # Keep preserve/use/restore together so another task cannot interleave a
    # network change that this example would then overwrite.
    async with session.transaction():
        original = await session.network
        try:
            await session.set_network(
                offline=False,
                latency=25,
                download_throughput=1024 * 1024,
                upload_throughput=512 * 1024,
            )
            conditions = await session.network
        finally:
            await session.set_network(
                offline=original.offline,
                latency=original.latency,
                download_throughput=original.download_throughput,
                upload_throughput=original.upload_throughput,
            )

    log_types = await session.log_types
    logs = await session.get_logs("browser") if "browser" in log_types else []
    return version, conditions, logs

Network latency is milliseconds; throughput is bytes per second. reset_network() restores default conditions; the example above preserves the caller's prior settings instead. Browser network emulation does not control driver downloads. Fetching logs consumes the returned entries. Avoid indiscriminately persisting logs from sensitive applications.

On an appropriate page origin, get_permission(name) returns a Permission or None; set_permission(name, state) accepts granted, denied, or prompt. For example, use geolocation and restore its previous state in finally. The setter owns its update and confirming observation as one transaction. Use your own session.transaction() when subsequent operations depend on that state. CDP command availability depends on the Chromium version; Aselenium does not guarantee every command across releases.

Casting APIs include cast_sinks, cast_issue, set_cast_sink(), start_casting(sink_name, mirror="tab"), and stop_casting(sink_name). These operate on real receivers and can expose browser/desktop content. Select a device deliberately; the local demo does not discover or start casting automatically.

Firefox: add-ons, context, and full-page capture

from pathlib import Path
from aselenium import FirefoxSession


async def use_temporary_addon(session: FirefoxSession, addon_path: str | Path):
    # Use a trusted .xpi file or an unpacked extension directory.
    addons = await session.install_addons(addon_path, temporary=True)
    try:
        return [addon.id for addon in addons]
    finally:
        for addon in addons:
            await session.uninstall_addon(addon)

session.addons is local add-on metadata. Add-ons must contain manifest.json with manifest_version 2 or 3; install.rdf and the old applications key are unsupported. Use browser_specific_settings.gecko.id; a driver-returned ID is used when a manifest ID is absent. Install only trusted extensions: they can access data according to their browser permissions.

await session.context reports "content" or "chrome". set_context() and reset_context() switch/restore it and observe the result under one transaction. Wrap any dependent browser work in your own transaction as well. Firefox's "chrome" context means privileged browser UI, not Google Chrome; it may need browser-specific startup permission and is not required for ordinary webpage automation. Prefer the content context.

Safari: system automation and permissions

Use Safari() with acquire(channel="stable"), or channel="dev" for Technology Preview. A custom driver= executable can be supplied to acquire(). Enable Remote Automation yourself before running; Aselenium's demo does not change system/browser automation settings.

Safari has its own permissions, get_permission(name), and set_permission(name, value) APIs, with boolean values, not Chromium's permission-state strings. Its options include automatic_inspection, automatic_profiling, and technology_preview. Permission updates serialize their read/merge/write/observation sequence to preserve other concurrent updates. Frames, W3C action chains, custom proxies, and PDF printing are disabled by the current facade. switch_frame() returns False; parent_frame() and default_frame() are no-ops that return True. Those return values do not establish that Safari changed frame focus.

Errors and logging

Errors are exported from aselenium. Distinguish provisioning/service failures from application-level element misses and ambiguous command timeouts.

Situation Examples / handling
Browser/driver unavailable BrowserBinaryNotDetectedError, DriverExecutableNotDetectedError; check executable, platform, selector, and cache.
Vendor request or download failed DriverRequestFailedError, DriverDownloadFailedError, DriverManagerTimeoutError; inspect the cause and network configuration.
Incompatible version InvalidDriverVersionError, IncompatibleWebDriverError; resolve a supported pair rather than suppress the error.
Driver service problem ServiceError and subclasses; inspect permissions, executable health, and service startup/cleanup.
Command budget expired SessionTimeoutError; the remote operation may have executed. Inspect state before deciding to retry.
Page interaction failed ElementStaleReferenceError, ElementClickInterceptedError, other WebDriverError subclasses; re-locate/wait or adjust the interaction.
Optional lookup missed find_element() returns None; an ordinary missing lookup is not necessarily an exception.
import logging
from aselenium import Session, SessionTimeoutError, WebDriverError


def configure_logging():
    logging.basicConfig(level=logging.INFO)
    logging.getLogger("aselenium").setLevel(logging.INFO)


async def load_with_diagnostics(session: Session, url: str):
    try:
        await session.load(url)
    except SessionTimeoutError:
        logging.getLogger(__name__).warning(
            "Navigation command timed out; inspect state before retrying"
        )
        raise
    except WebDriverError:
        logging.getLogger(__name__).warning("Browser rejected navigation")
        raise

The library installs a NullHandler; the application controls logging output. Command logs omit request bodies, query strings, and proxy credentials. Review exception text and application logs separately before sharing diagnostics.

Provisioning GETs retry a limited set of transient network/HTTP failures under a shared deadline. Authentication/TLS failures are not treated as cache misses. Mutating WebDriver commands are not automatically replayed after an ambiguous transport failure. Do not use broad retries around clicks, uploads, submissions, or other operations that may already have succeeded remotely.

Runnable demo

Two named demos distinguish repeatable local checks from live website automation:

Demo Website Default browser mode Purpose
demo_local.py Local HTML served on loopback Headless, except Safari Full selectable package feature tour.
demo_google.py Real Google.com Visible / headed Open Google, optionally submit one search, and capture the live page.

Use your project interpreter explicitly on macOS/Linux (on Windows, use .venv\Scripts\python.exe), or activate the environment before using python. Run commands from the repository root. The first run may need --allow-download to populate the demo cache; later runs can use the compatible cached driver.

Local HTML feature tour

src/demo_local.py serves bundled HTML on an ephemeral 127.0.0.1 port and controls a real browser against it. It does not navigate to Google or another public website. Start with:

.venv/bin/python src/demo_local.py list
.venv/bin/python src/demo_local.py run --browser chrome --allow-download

Once provisioned, run offline, make the browser visible, or select chapters:

.venv/bin/python src/demo_local.py run --browser chrome
.venv/bin/python src/demo_local.py run --browser chrome --headed
.venv/bin/python src/demo_local.py run --sections concurrency cancellation --profile-demo

For a Snap/Flatpak Firefox, create a shared directory once and pass it explicitly:

mkdir -p "$HOME/aselenium-firefox-profiles"
.venv/bin/python src/demo_local.py run --browser firefox --allow-download \
  --profile-root "$HOME/aselenium-firefox-profiles" --session-timeout 60

Driver management and options run first. The 13 selectable chapters cover navigation, elements, waits, cookies, windows, frames, alerts, scripts, actions, artifacts, vendor commands, concurrency, and cancellation: 15 stages in a full tour. Browser-specific limitations are reported as skipped, not counted as passes. The --profile-demo option clones a new empty profile template, never your personal profile. The local tour's --session-timeout option is the per-command and session-start deadline (30 seconds by default); a cold Snap/Flatpak Firefox launch may need a larger value such as 60 seconds. Its separate --timeout option bounds the whole tour and defaults to 240 seconds.

To provision without opening a browser:

.venv/bin/python src/demo_local.py install --browser chrome --allow-download

See the local HTML guide for all CLI options, browser prerequisites, exact-version policies, and Chrome for Testing.

Real-world Google demo

src/demo_google.py visits https://www.google.com/ in visible Chrome by default. Without --query, it opens only the homepage, waits for an enabled, unobscured search field, records the page URL/title, saves google-home.png, and leaves the page open for five seconds before cleanup:

.venv/bin/python src/demo_google.py run --browser chrome --allow-download

With a compatible cached driver, submit one optional search or run headless:

.venv/bin/python src/demo_google.py run --query "Aselenium Python" --hold-seconds 10
.venv/bin/python src/demo_google.py run --headless --hold-seconds 0

The search example uses clear() and send(..., KeyboardKeys.ENTER), then waits for a Google /search URL carrying the query and visible result headings. On success, it records up to five heading samples and saves google-results.png. It does not click result links or ads. The wait can fail if Google changes its layout, returns no results, or restricts the request.

If a consent dialog appears in a visible browser, make your own choice while the readiness wait is active. Allow more time when needed:

.venv/bin/python src/demo_google.py run --wait-timeout 60 --hold-seconds 10

The demo does not select consent, sign in, solve CAPTCHA, or retry a blocked search. Its key options are:

Option Behavior
--browser chrome|chromium|edge|firefox|safari Select the installed browser; Chrome is the default.
--binary PATH Use an explicit executable, such as the executable inside a macOS .app bundle.
--query TEXT Submit one search; omit for homepage-only navigation.
--headless Hide the window; Safari is always headed.
--hold-seconds N Wait on the final/attention page before cleanup; default 5, range 0–60.
--wait-timeout N Readiness budget per page in seconds; default 20.
--timeout N Overall work budget in seconds; default 180. Owned cleanup can take longer.
--allow-download Allow driver vendor requests during provisioning, not a toggle for website access.
--cache-dir PATH / --output-dir PATH Override the shared demo cache parent or run-output parent.

Recorded live result, 2026-09-04: the homepage-only run passed in visible Chrome. One search encountered Google's unusual-traffic CAPTCHA; the demo saved an attention screenshot and exited with needs-attention. A normal live search results page was not verified in that run. See the Google guide and validation record for the exact environment and evidence boundaries.

Demo outputs and network behavior

Both entry points print help without launching a browser when run with no arguments:

.venv/bin/python src/demo_local.py
.venv/bin/python src/demo_google.py

Both share .demo-cache under the checkout, separate from the minimal examples' ./browser-cache and the library's default ~/.aselenium cache. By default, the demo commands prohibit driver vendor requests; use --allow-download to enable compatible provisioning. This restriction is a demo CLI policy, not the library's default behavior for online selectors. Neither offline driver resolution nor local HTML prevents browser-internal background network activity.

Each run writes a unique local-<browser>-... or google-<browser>-... directory under .demo-output, so separate runs do not overwrite one another:

Output Contents
report.json Selected/actual versions, run status, diagnostics, and artifact names; local runs also include per-stage results.
Local page.png / page.pdf Fixture captures from the artifact chapter, where supported by the selected browser/mode.
Google google-home.png Homepage after its search field becomes usable.
Google google-results.png Results page only when the optional search completes.
Google google-attention.png Best-effort capture of a challenge, consent timeout, or other workflow error.

For the Google demo, exit 0 means completion, 2 means the page needs attention, 1 means another failure, and 130 means keyboard interruption. Homepage-only success does not claim that searching was tested. Reports and captures can contain query text, page content, local paths, and IP/network diagnostics; review them before sharing.

Use src/demo_local.py or src/demo_google.py in scripts and documentation; the former src/demo.py entry point is no longer present in this checkout. The demos, shared helper, and fixtures remain repository examples; production wheels and source distributions intentionally contain only the package and its required metadata and runtime resources. See the demo overview to compare the two workflows.

Troubleshooting

Dependency import errors

If you see ModuleNotFoundError: No module named 'orjson' despite installing it in .venv, first check whether the failing script actually uses that environment. An installed dependency is available to its interpreter, not every Python on the machine. From the repository root, compare:

python -c "import sys; print(sys.executable)"
.venv/bin/python -c "import sys, orjson; print(sys.executable); print(orjson.__file__)"
.venv/bin/python -m pip show aselenium orjson

Run the script with .venv/bin/python, or set your editor's interpreter to the same executable. If imports still fail under that exact interpreter, install the checkout there with .venv/bin/python -m pip install -e . and read any dependency installation error. Do not add arbitrary sys.path entries or suppress the missing import. The file path in a traceback identifies the source that was loaded, not necessarily the interpreter that loaded it.

Browser, cache, and website problems

Symptom What to check
No browser window appears The local tour is headless by default; add --headed. The Google demo is visible unless --headless is set. With no subcommand, either script only prints help.
Browser executable is not detected Install the browser or provide --binary to the demo; supply the executable, not a directory or .app bundle.
Offline provisioning reports no compatible driver Confirm the selected --cache-dir and browser version. Run once with --allow-download when vendor access is permitted, especially after a browser update.
Google opens but search stops with exit code 2 Read report.json and inspect google-attention.png. Consent, CAPTCHA, redirects, or layout changes can prevent completion; the demo does not bypass restrictions.
Google consent needs more time Use a headed run with --wait-timeout 60 and choose manually; --hold-seconds controls the final pause, not readiness time.
A script immediately closes its browser Leaving async with closes the session. The Google CLI provides --hold-seconds for observation; your own script should await its work inside the context.
Safari rejects a feature Review the Safari boundaries, enable Remote Automation yourself, and do not pass Chromium-only options.

Avoid deleting caches as a first response to errors. Record the interpreter, browser/driver versions, selected cache, and exact failure before deciding on a scoped repair. See cache recovery.

Cache recovery

SQLite v2 is the only cache format. Old Feather metadata is neither read nor imported; the import module and extra have been removed. Existing files outside the v2 cache are left untouched. Reprovision a supported browser/driver pair with install_result(policy="compatible-build", validate_compatibility=True) for Chrome/Chromium/Edge, or policy="cached-compatible" for Firefox. See driver management for selectors and offline behavior.

Cache publication is staged and atomic. Matching orphan publications can be reindexed after a crash. Advanced scoped recover() and clean_staging() maintenance APIs are explained in the cache recovery guide. Run synchronous maintenance with asyncio.to_thread() in async applications. Do not delete an entire cache or its persistent lock files as a routine repair. NFS/SMB, multi-host caches, hostile concurrent writers, and hardware power-loss durability are outside the verified support boundary.

Compatibility and verification

The public API targets Python 3.10+ and five browser facades. Declared support, configured CI jobs, and results from a particular machine are different things. See the pre-deployment review for the latest tested candidate, exact counts, installed-wheel checks, and remaining deployment gates. Older reports are dated snapshots, not current badges.

Area Capability and verification boundary
Chrome and Edge Shared Chromium automation, CDP, network emulation, permissions, logging, and PDF output. Installed-wheel local tours and reliability harnesses exist; the latest review identifies which were rerun.
Chromium and Chrome for Testing Standalone Chromium and managed CfT browser/driver pairs are supported by the manager. Provider, cache, and command regressions do not substitute for native acceptance of a separate browser bundle.
Firefox WebDriver automation, action chains, add-ons, contexts, and full-page screenshots. The local tour checks the applicable features; driver resolution fails closed for unrecorded future Gecko releases.
Safari macOS system automation with Remote Automation enabled beforehand. The current facade disables frames, action chains, and concurrent acquisitions; the tour records these as three exclusions, not passes. Custom proxies and PDF printing are also unavailable.
Python and operating systems The configured CI matrix covers Python 3.10–3.14, Linux/Windows/macOS, and exact runtime minimum dependencies. A local Python/macOS pass does not establish a pass for the other jobs.
Native CI Installed-wheel jobs cover Chrome/Firefox on Linux and macOS, Edge on Windows, and Safari on macOS. Chrome/Edge reliability checks include recovery, proxy routing, and sustained use; scheduled and release runs request a 600-second soak.
Real Google website A dated visible-Chrome homepage run passed. An optional search encountered CAPTCHA and stopped with needs-attention; a normal live results-page pass is not claimed.
Not locally certified Other OS/browser/version combinations, Linux Snap/Flatpak profile access, separate Chromium/CfT bundles, WebView2, beta/dev channels, and casting hardware need environment-specific acceptance.

The regular test suite includes deterministic regressions and real disposable loopback HTTP/TCP/TLS tests. Native acceptance imports a built wheel from outside the checkout and checks its origin. Coverage, typing, executable examples, and metadata checks are separate gates; none alone proves full feature correctness.

Before deployment, run the configured CI/release gates on the exact commit and test the installed distribution on your target platform. A bounded soak does not prove unlimited uptime or an absence of leaks. SHA checks and safe extraction do not turn browser automation into a security sandbox for untrusted websites.

The earlier production review, release-acceptance guide, and Google validation record retain the history and reproduction instructions without implying that every earlier environment was rerun for the latest source changes.

Development

Install development dependencies into the same environment as the checkout, then run the quality gates. Commands below use macOS/Linux executable paths; substitute .venv\Scripts\python.exe on Windows:

.venv/bin/python -m pip install -r requirements-dev.txt
.venv/bin/python -m pytest --asyncio-debug
.venv/bin/python -m ruff check src tests scripts
.venv/bin/python -m ruff format --check src tests scripts
.venv/bin/python scripts/check_api_quality.py
.venv/bin/python scripts/check_example_contracts.py
.venv/bin/python -m mypy
.venv/bin/python -m coverage run --branch --source=aselenium -m pytest --asyncio-debug
.venv/bin/python -m coverage report
.venv/bin/python -m pip check
.venv/bin/python -m pip_audit --skip-editable
.venv/bin/python -m build --no-isolation
.venv/bin/python -m twine check --strict dist/*

Default tests prohibit external network requests and installed-browser launches. The loopback tests use real HTTP/TCP and HTTPS against their own strictly allowlisted temporary local servers and require permission to bind a local socket. In a restricted environment, -m "not loopback" explicitly deselects those tests; it does not establish their behavior. See the feature-testing guide. Some tests use controlled subprocesses for cache concurrency and distribution builds. Installed-wheel browser checks and the individual crash/proxy/soak harnesses are described in the release-acceptance guide. They use controlled local pages and dedicated caches/profiles. Browser launches remain opt-in outside the explicitly configured native CI jobs. Google demo tests use fake sessions; passing them does not imply that Google allowed a live search.

Strict expected failures report known defects separately from passing tests. An unexpected pass fails the run so its marker must be revisited after a fix. There are currently no expected-failure markers: the previously identified issue families and the additional final-review fixes have passing regressions. A green local suite still does not certify unavailable native targets or every production deployment; see the final review and acceptance guide.

For a focused documentation/demo check:

.venv/bin/python -m pytest tests/test_examples.py tests/test_demo_local.py tests/test_demo_google.py -q

README checks compile/import Python examples, validate known API call signatures and local links, and exercise selected recipes without launching browsers.

All maintained Python definitions have Google-style docstrings and annotated signatures. Ruff enforces import ordering, unused-import detection, documentation style, and annotation coverage. The structural audit additionally rejects imports inside functions and enforces the public-PathInput/internal-Path architecture, including a guard against stringifying and reparsing paths. Module-level platform guards and TYPE_CHECKING imports remain intentional. See the contributor API-quality guide.

Docstring examples use singular Example: with Python >>> and ... prompts. They are syntax-checked and checked against resolvable API signatures. The added runtime suite executes 38 distinct docstrings verbatim with controlled fixtures, in addition to existing pure examples and README recipe checks. See the guide for required setup and execution limits.

Mypy checks the complete package, both executable demos, their shared support module, the quick-start program, and all maintained release/acceptance scripts under src/ and scripts/. An additional installed-wheel consumer gate verifies public asynchronous and path return types and requires deliberate API misuse to fail type checking. Dynamic WebDriver/JavaScript data, extension arguments, and test doubles still use Any only where their shape is intentionally open. Runtime/resource metadata lives in pyproject.toml. Distribution tests build a wheel and sdist, rebuild the wheel from the sdist, and verify imports/resources outside the checkout. Local builds do not publish a release. Follow the configured gated release workflow and verify its external settings before publishing.

Further documentation

License and acknowledgements

Aselenium is distributed under the Apache License 2.0. The accompanying NOTICE preserves the attribution referenced by inherited source headers.

It uses aiohttp, psutil, and orjson, and draws inspiration and adapted code from arsenic, Selenium, and webdriver-manager. Original source-file license and attribution notices are retained.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aselenium-2.0.0.tar.gz (259.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aselenium-2.0.0-py3-none-any.whl (247.5 kB view details)

Uploaded Python 3

File details

Details for the file aselenium-2.0.0.tar.gz.

File metadata

  • Download URL: aselenium-2.0.0.tar.gz
  • Upload date:
  • Size: 259.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for aselenium-2.0.0.tar.gz
Algorithm Hash digest
SHA256 80d04a6144e3569524c562eb8a3a152e896e2f232d44c8351b2cb13b5f73d70c
MD5 fb9eb92423d060c98682f83b7c2a1883
BLAKE2b-256 a9c21046b51ee6d58acd7abd82232106701dc522bbf43ee8c3a06b9551fea0bc

See more details on using hashes here.

File details

Details for the file aselenium-2.0.0-py3-none-any.whl.

File metadata

  • Download URL: aselenium-2.0.0-py3-none-any.whl
  • Upload date:
  • Size: 247.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for aselenium-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3cc7a988448ea79665f72695b6f170fcdb3c8d83df3cc3453c53e71d622ca11f
MD5 35d97bc4dc1b9d2b9a7502cbfe208098
BLAKE2b-256 a99f30a04241c9c3466c56f6fc6893e288a64c00f02e5d8fa0996c48d2273b88

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

2.0.0 This release

2 files

1.0.5

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page