This release is a pre-release and may not be stable for production use.
Lithium
Version – 1.0b1
An HTTP-first, high stealth web runtime for Python.
The simplicity of requests + the interaction model of Selenium, backed by a tiny native Go runtime — no browser to install, no drivers, no config to get going.
from lithium import Client
client = Client() # zero-config, undetected out of the box
page = client.get("https://example.com/login")
page.fill("#username", "alice")
page.fill("#password", "secret")
page.click("button[type=submit]")
page.wait_for("#dashboard")
print(page.text)
page.screenshot("dashboard.png")
Fun fact #1: the name comes from lithium — the lightest solid element — because the whole runtime binary is only ~19 MB. Fun fact #2: Python talks to the Go runtime over a Unix socket using MessagePack. No browser process, no WebDriver, no Chromium. Just a runtime. Fun fact #3: by default the runtime speaks a Chrome TLS (JA3) fingerprint over uTLS, so
Client()is already impersonating a real browser — you don't have to turn anything on.
Installation
pip install lithium-web
That's it.
Quickstart — major features, individually
Requests-style HTTP
from lithium import Client
client = Client()
r = client.get("https://api.example.com/users?limit=10")
print(r.status_code) # 200
print(r.ok) # True
print(r.json()) # parsed body
# POST with JSON
r = client.post("https://api.example.com/users", json={"name": "Ada"})
# POST with a form body (auto urlencoded)
r = client.post("https://httpbin.org/post", data={"q": "lithium"})
# POST multipart upload
r = client.post("https://httpbin.org/post",
data={"note": "hi"},
files={"file": ("report.pdf", b"%PDF-1.4 ...")})
Selenium-style page interaction
from lithium import Client
page = client.get("https://example.com/login")
page.fill("#username", "alice")
page.fill("#password", "secret")
page.click("button[type=submit]") # navigates; `page` updates in place
page.wait_for("#dashboard") # waits for the element to appear
print(page.text) # text of the current page
Execute JavaScript
page = client.get("https://example.com")
page.execute_script("document.body.dataset.loaded = 'true'")
print(page.dom.query("body").get_attr("data-loaded")) # 'true'
Query the DOM
page = client.get("https://example.com/products")
items = page.dom.query_all(".product")
for el in items:
print(el.text, el.get_attr("data-price"))
Screenshots
page = client.get("https://example.com")
page.screenshot("page.png") # writes a PNG, returns the path
png_bytes = page.screenshot() # or get raw bytes
Async client
import asyncio
from lithium import AsyncClient
async def main():
async with AsyncClient() as client:
r = await client.get("https://example.com")
print(r.status_code)
asyncio.run(main())
Streaming downloads
client = Client()
client.stream_to_file("https://example.com/bigfile.bin", "bigfile.bin")
Proxies & rotating proxies
# single proxy
client = Client(proxy="socks5://user:pass@host:1080")
# rotating proxy pool (round-robin per request)
client = Client(proxies=[
"socks5://u:p@proxy1:1080",
"socks5://u:p@proxy2:1080",
])
Custom CA & mutual TLS
client = Client(
ca_cert="/path/to/ca.pem",
client_cert="/path/to/client.crt",
client_key="/path/to/client.key",
)
Geo / locale auto-detection
client = Client(auto_location=True) # timezone + currency + country
r = client.get("https://example.com")
print(r.timezone, r.currency, r.country_code)
Network inspection
page = client.get("https://example.com")
for req in page.network.requests: # every HTTP + in-page fetch/XHR
print(req.method, req.status, req.url)
Custom browser profile / fingerprint
# pick a coherent identity (UA + JA3 + navigator + GPU all agree)
client = Client(platform="windows", browser="chrome", fingerprint="my-identity")
One script that uses everything
import asyncio, json
from lithium import Client, AsyncClient
# 1. Requests-style client, undetected by default
client = Client(proxies=["socks5://u:p@h1:1080", "socks5://u:p@h2:1080"])
client.get("https://example.com/set-session")
# 2. A page we interact with, like Selenium
page = client.get("https://example.com/login")
page.fill("#username", "alice")
page.fill("#password", "secret")
page.check("#terms")
page.click("button[type=submit]")
page.wait_for("#dashboard")
print("landed:", page.status_code)
# 3. DOM + JS
rows = page.dom.query_all("table tbody tr")
page.execute_script("document.title = 'hello from js'")
# 4. Run JS that does real network (in-page fetch)
page.execute_script(
"fetch('/api/data').then(r=>r.json())"
".then(d=>document.body.setAttribute('data-h', d.hello));"
)
# 5. Rich media + screenshot
print("media:", page.media) # discovered video/audio/img sources
page.screenshot("dashboard.png")
# 6. Inspect every request the session made (HTTP + JS-driven)
for req in page.network.requests:
print(" net:", req.method, req.status, req.url)
# 7. Large download, streamed to disk
client.stream_to_file("https://example.com/db.sqlite", "db.sqlite")
# 8. Plain HTTP / JSON when you don't need a page
r = client.post("https://example.com/api", json={"a": 1})
print(r.json())
# 9. Async when you need concurrency
async def main():
async with AsyncClient() as ac:
results = await asyncio.gather(*[ac.get("https://example.com") for _ in range(5)])
print([r.status_code for r in results])
asyncio.run(main())
Config reference
All options work as direct keyword args to Client(...) or via a Config/JSON file.
| Option | Default | What it does |
|---|---|---|
profile |
"chrome-windows" |
Coherent browser identity (UA + headers + JA3 + navigator). |
fingerprint |
None |
Persistent identity name; same name = same profile across runs. |
platform |
None |
Force windows / macos / linux / android / ios. |
browser |
None |
Force chrome / firefox / opera / brave. |
proxy |
None |
Single proxy URL (http/https/socks4/socks4a/socks5/socks5h). |
proxies |
None |
List of proxy URLs, rotated round-robin per request. |
verify_ssl |
True |
Verify server certificates. |
ca_cert, client_cert, client_key |
None |
Custom CA bundle and client certificate (mTLS). |
trust_env |
True |
Respect HTTP_PROXY/HTTPS_PROXY/NO_PROXY env vars. |
tls_spoof |
None |
Explicitly enable/disable TLS (JA3) impersonation. |
http3 |
False |
Try HTTP/3 (QUIC) with graceful h1/h2 fallback. |
execute_scripts |
False |
Auto-run inline <script> on page load. |
js_timeout |
10.0 |
Max seconds a JS call may run. |
timeout |
30.0 |
Default request timeout (seconds). |
max_redirects |
0 |
Redirect limit (0=Go default 10, -1=don't follow). |
max_idle_conns, max_idle_conns_per_host |
100, 10 |
HTTP connection-pool sizing. |
viewport_width, viewport_height |
platform default | Screen/inner size (screen, innerWidth, matchMedia). |
device_scale_factor |
platform default | devicePixelRatio (mobile auto-sets to 2). |
screen_color_depth |
24 |
screen.colorDepth / pixelDepth. |
hardware_concurrency, device_memory |
from fingerprint | navigator.hardwareConcurrency / .deviceMemory. |
max_touch_points |
0 |
navigator.maxTouchPoints. |
do_not_track |
None |
navigator.doNotTrack. |
audio_sample_rate |
44100 |
AudioContext().sampleRate. |
screenshot_width, screenshot_height |
1280, 800 |
PNG size for page.screenshot(). |
virtual_gpu |
True |
Enable virtual WebGL context. |
canvas_noise |
True |
Make toDataURL vary per identity. |
auto_location, auto_timezone, auto_currency, auto_language |
None |
Auto-detect locale from your IP (via geo_url). |
geo_url |
ip-api.com | Custom geo lookup endpoint. |
storage_file |
None |
Persist localStorage across runs/sessions. |
socket_timeout |
None |
Seconds to wait for a reply from the runtime. |
shared |
True |
Reuse one runtime process across clients. |
binary |
auto | Path to the runtime binary (override). |
verbose |
False |
Print go-side logs to stderr. |
solution |
False |
Attach live DNS/TCP/TLS diagnostics to errors. |
Why lithium (comparison)
| Capability | lithium | primp | tls_client | curl_cffi |
|---|---|---|---|---|
| TLS (JA3) impersonation | ✅ | ✅ | ✅ | ✅ |
| Coherent multi-layer identity (UA↔headers↔navigator↔GPU) | ✅ | partial | partial | partial |
Selenium-style interaction (click/fill/wait_for/select) |
✅ | ❌ | ❌ | ❌ |
Full DOM querying (query/query_all) |
✅ | ❌ | ❌ | ❌ |
| JavaScript execution | ✅ | ❌ | ❌ | ❌ |
In-page fetch()/XHR → real HTTP |
✅ | ❌ | ❌ | ❌ |
localStorage/indexedDB/WebSocket/WebCrypto |
✅ | ❌ | ❌ | ❌ |
| Screenshots | ✅ | ❌ | ❌ | ❌ |
Network inspection (page.network.requests) |
✅ | ❌ | ❌ | ❌ |
| Zero-config browser identity (no libs/browser install) | ✅ | ❌ | ❌ | ❌ |
| Async | ✅ | ✅ | ✅ | ✅ |
| HTTP/2 fingerprint (Chrome SETTINGS) | partial | ✅ | ✅ | ✅ |
| HTTP/3 | opt-in | ✅ | ✅ | ✅ |
| Streaming downloads | ✅ | ✅ | ✅ | ✅ |
| Proxy rotation / mTLS / CA | ✅ | ✅ | ✅ | ✅ |
High stealth: Sannysoft proof
The runtime drives the official bot.sannysoft.com page and reads its own detection table: 8 / 8 passed, 0 failed. Run it yourself:
import json, re
from lithium import Client
with Client(execute_scripts=False, browser="chrome", platform="windows") as client:
page = client.get("https://bot.sannysoft.com/")
html = page.text
# the site's own detection needs lodash (it uses _.has on navigator.webdriver)
page.execute_script(
client.get("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js", timeout=15).text
)
# run the site's OWN detection script (fills the real main result table)
for s in re.findall(r'<script(?![^>]*src)[^>]*>(.*?)</script>', html, re.S):
if "runBotDetection" in s:
try:
page.execute_script(s) # the site's real detection code
except Exception:
pass # harmless canvas-hash noise; table already filled
break
counts = json.loads(page.execute_script(
"JSON.stringify({passed: document.querySelectorAll('.result.passed').length,"
" failed: document.querySelectorAll('.result.failed').length})"))
print(f"OFFICIAL sannysoft: {counts['passed']} passed, {counts['failed']} failed")
Output: OFFICIAL sannysoft: 8 passed, 0 failed.
Proof of undetected: the default
Client()also speaks a Chrome JA3 (e.g.771,4865-4866-4867-49195-49199-...) verified againsthttps://tls.peet.ws/api/all, and the JA3 version matches the UA version.
Every feature at a glance
HTTP
get/post/put/patch/delete/head/options- Bodies: plain bytes/str, JSON (
json=), urlencoded (data=dict), multipart (files=) - Cookies persisted per session automatically
- Redirect policy (
max_redirects) - Rotating proxy pools (
proxies=[...]) - Connection pooling tuning (
max_idle_conns*) - Streaming downloads (
stream_to_file) - Custom CA + mTLS (
ca_cert/client_cert/client_key) - Geo / locale auto-detect (
auto_location+ friends, customgeo_url) - Network inspection (
client.network,page.network.requests) - Async client (
AsyncClient) - HTTP trailers (
resp.trailers)
Browser runtime
- DOM:
page.dom.query/query_all, attributes, text, html, events - Interaction:
fill,type,click,submit,select_option,check,uncheck,clear,wait_for,goto - JavaScript:
page.execute_script, auto-run inline scripts - In-page
fetch()/XMLHttpRequest→ real HTTP - Storage:
localStorage,sessionStorage(optionally persisted) - APIs:
WebCrypto,MutationObserver,History/location,WebSocket,IndexedDB,matchMedia,getComputedStyle,performance,permissions,getBattery - Virtual graphics: Canvas 2D, WebGL,
AudioContext - Screenshots (
page.screenshot) - Media discovery + download (
page.media,download_media) - Coherent fingerprints (UA ↔ JA3 ↔
navigator↔ GPU ↔ screen)
API reference — unique functions
Client
client.get/post/put/patch/delete/head/options(...)client.download(url, path)— fetch and write to diskclient.stream_to_file(url, path)— stream response straight to diskclient.network—NetworkLogof every request (iterable,.requests,.responses)client.close()/ context manager
Response (a.k.a. the page)
status_code,headers,trailers,url,elapsed_ms,ok,text,json(),contenttimezone,currency,country_code(when auto-location on)dom→ query/query_all/wait_forexecute_script(js)fill,type,click,submit,select_option,check,uncheck,clearwait_for(selector, timeout=),goto(url)(both update the page in place)screenshot(path=None)→ writes/returns PNGnetwork→NetworkLogmedia→ discovered<video>/<audio>/<img>sourcesdownload(url, path),download_media(dir, kinds=None)stream(chunk_size=)— chunked iterator
Element
text,html,get_attr(name, default=None),set_attr(name, value),remove()add_event_listener(event, cb),dispatch_event(event, detail=None)click(),fill(v),type(text, delay=),select_option(v),clear(),check(),uncheck(),submit()
AsyncClient
awaitversions ofget/post/put/patch/delete/head/options/stream_to_file/download, plusnetwork.
Made by Blaze, available on discord & github
feel free to contact me regarding any issues, suggestions or queries
License & note
lithium-web is under active development. The name, PyPI, and trademark availability should be confirmed before any public release, since similarly-named projects already exist.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file lithium_web-1.0b1.tar.gz.
File metadata
- Download URL: lithium_web-1.0b1.tar.gz
- Upload date:
- Size: 37.5 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2b60b7433b29d479c6d2c22bc8ce41e558efee78811e331e2b70e67e225ba28a
|
|
| MD5 |
671227eef5bc2c27ae949d78f61564ac
|
|
| BLAKE2b-256 |
fb81950c44584a1b87cb434406492128326b602aca2e4cc606c8ae9cde697ca5
|
File details
Details for the file lithium_web-1.0b1-py3-none-any.whl.
File metadata
- Download URL: lithium_web-1.0b1-py3-none-any.whl
- Upload date:
- Size: 37.7 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
473167c66e7a3db5b0c6d6c5182c4cd7a36ab4fbde964f9587354cc3b2b4492c
|
|
| MD5 |
9e7ddd27f686363dc50d93b875d5e9c6
|
|
| BLAKE2b-256 |
0cdceca6b1713ae6c0b7ecbe7319a9a4a7ba4ae34cfe3e93e0e621a975aa8a3e
|