Modern Python wrapper for tls-client with an httpx-like API and browser TLS fingerprinting.
- httpx-like API - Familiar
Client/AsyncClientpattern - Browser TLS fingerprinting - 50+ profiles (Chrome, Firefox, Safari, Opera)
- Auto-download binaries - No manual setup required
- True async support - Using anyio (works with asyncio and trio)
- Fixes common wrapper bugs:
cookies.clear()actually works- No memory leaks (proper cleanup with context managers)
- Case-insensitive headers (RFC 7230 compliant)
pip install tlshttpOr with uv:
uv add tlshttpimport tlshttp
with tlshttp.Client() as client:
response = client.get("https://httpbin.org/get")
print(response.json())import asyncio
import tlshttp
async def main():
async with tlshttp.AsyncClient() as client:
response = await client.get("https://httpbin.org/get")
print(response.json())
asyncio.run(main())client = tlshttp.Client(
# Browser fingerprint profile
profile="chrome_120", # Default
# Request defaults
timeout=30.0,
follow_redirects=True,
# Proxy support
proxy="http://user:pass@proxy.example.com:8080",
# TLS settings
verify=True, # Verify SSL certificates
http2=True, # Enable HTTP/2
# Default headers for all requests
headers={"User-Agent": "MyApp/1.0"},
# Default cookies
cookies={"session": "abc123"},
# Base URL for relative paths
base_url="https://api.example.com",
)All standard HTTP methods are supported:
with tlshttp.Client() as client:
# GET
response = client.get("https://httpbin.org/get")
# POST with JSON
response = client.post(
"https://httpbin.org/post",
json={"key": "value"}
)
# POST with form data
response = client.post(
"https://httpbin.org/post",
data={"username": "john", "password": "secret"}
)
# PUT
response = client.put(
"https://httpbin.org/put",
json={"updated": True}
)
# PATCH
response = client.patch(
"https://httpbin.org/patch",
json={"field": "new_value"}
)
# DELETE
response = client.delete("https://httpbin.org/delete")
# HEAD
response = client.head("https://httpbin.org/get")
# OPTIONS
response = client.options("https://httpbin.org/get")response = client.get(
"https://httpbin.org/get",
params={"page": 1, "limit": 10}
)
# Requests: https://httpbin.org/get?page=1&limit=10response = client.get(
"https://httpbin.org/headers",
headers={"Authorization": "Bearer token123"}
)response = client.post(
"https://httpbin.org/post",
json={"message": "Hello", "count": 42}
)response = client.post(
"https://httpbin.org/post",
data={"username": "john", "password": "secret"}
)response = client.post(
"https://httpbin.org/post",
content=b"raw bytes here"
)# Per-request timeout (overrides client default)
response = client.get(
"https://httpbin.org/delay/5",
timeout=10.0
)# Basic auth - httpx-style tuple
response = client.get(
"https://httpbin.org/basic-auth/user/pass",
auth=("user", "pass")
)response = client.get("https://httpbin.org/get")
# Status
response.status_code # 200
response.is_success # True (2xx)
response.is_redirect # False (3xx)
response.is_client_error # False (4xx)
response.is_server_error # False (5xx)
response.is_error # False (4xx or 5xx)
# Content
response.content # Raw bytes
response.text # Decoded string (auto-detects encoding)
response.json() # Parse as JSON
# Headers (case-insensitive)
response.headers["Content-Type"]
response.headers["content-type"] # Same value
response.headers.get("X-Custom", "default")
# Cookies from response
response.cookies["session_id"]
# URL (final URL after redirects)
response.url
# HTTP version
response.http_version # "HTTP/2" or "HTTP/1.1"
# Raise exception for error status codes
response.raise_for_status() # Raises HTTPStatusError for 4xx/5xxCookies are automatically stored and sent with subsequent requests:
with tlshttp.Client() as client:
# Server sets a cookie
client.get("https://httpbin.org/cookies/set/session/abc123")
# Cookie is automatically sent
response = client.get("https://httpbin.org/cookies")
print(response.json()) # {"cookies": {"session": "abc123"}}# Set cookies on client
client.cookies["token"] = "xyz789"
client.cookies.set("user", "john", domain="example.com")
# Get cookies
session = client.cookies.get("session")
# Clear all cookies (actually works, unlike other wrappers!)
client.cookies.clear()
# Clear cookies for specific domain
client.cookies.clear(domain="example.com")
# Iterate cookies
for name, value in client.cookies.items():
print(f"{name}: {value}")
# Export to dict
all_cookies = client.cookies.to_dict()HTTP headers are case-insensitive per RFC 7230. tlshttp handles this correctly:
from tlshttp import Headers
headers = Headers({"Content-Type": "application/json"})
# All of these return the same value
headers["Content-Type"]
headers["content-type"]
headers["CONTENT-TYPE"]client = tlshttp.Client(
headers={"User-Agent": "MyBot/1.0", "Accept": "application/json"}
)
# Per-request headers merge with (and override) client defaults
response = client.get(
"https://httpbin.org/headers",
headers={"Accept": "text/html"} # Overrides Accept, keeps User-Agent
)tlshttp includes 50+ browser TLS fingerprint profiles that mimic real browsers:
client = tlshttp.Client(profile="chrome_120") # Recommended default
client = tlshttp.Client(profile="chrome_131") # Newest
client = tlshttp.Client(profile="chrome_103") # Oldest supportedAvailable: chrome_103 through chrome_131, plus PSK variants like chrome_120_psk
client = tlshttp.Client(profile="firefox_120")
client = tlshttp.Client(profile="firefox_133") # Newest
client = tlshttp.Client(profile="firefox_102") # OldestAvailable: firefox_102 through firefox_133
client = tlshttp.Client(profile="safari_16_0")
client = tlshttp.Client(profile="safari_ios_16_0")
client = tlshttp.Client(profile="safari_ios_17_0")client = tlshttp.Client(profile="opera_89")
client = tlshttp.Client(profile="opera_90")
client = tlshttp.Client(profile="opera_91")from tlshttp.profiles import BROWSER_PROFILES
for name in sorted(BROWSER_PROFILES.keys()):
print(name)The AsyncClient has the same API as Client, but all methods are async:
import asyncio
import tlshttp
async def main():
async with tlshttp.AsyncClient(profile="firefox_120") as client:
# Single request
response = await client.get("https://httpbin.org/get")
# Concurrent requests
urls = [
"https://httpbin.org/get?id=1",
"https://httpbin.org/get?id=2",
"https://httpbin.org/get?id=3",
]
tasks = [client.get(url) for url in urls]
responses = await asyncio.gather(*tasks)
for response in responses:
print(response.json()["args"]["id"])
asyncio.run(main())Control the maximum number of concurrent requests:
# Limit to 5 concurrent requests (default: 10)
client = tlshttp.AsyncClient(max_concurrent=5)# HTTP proxy
client = tlshttp.Client(proxy="http://proxy.example.com:8080")
# With authentication
client = tlshttp.Client(proxy="http://user:pass@proxy.example.com:8080")
# SOCKS5 proxy
client = tlshttp.Client(proxy="socks5://proxy.example.com:1080")import tlshttp
try:
with tlshttp.Client(timeout=5.0) as client:
response = client.get("https://httpbin.org/delay/10")
response.raise_for_status()
except tlshttp.TimeoutError:
print("Request timed out")
except tlshttp.ConnectError:
print("Failed to connect")
except tlshttp.HTTPStatusError as e:
print(f"HTTP {e.response.status_code}: {e.response.text}")
except tlshttp.RequestError as e:
print(f"Request failed: {e}")TLSHTTPError (base)
├── RequestError
│ ├── ConnectError
│ ├── TimeoutError
│ └── ProxyError
└── HTTPStatusError (raised by raise_for_status())
| Feature | tlshttp | requests | httpx | curl_cffi | tls-client |
|---|---|---|---|---|---|
| TLS Fingerprinting | ✅ 50+ | ❌ | ❌ | ✅ | ✅ |
| Async Support | ✅ | ❌ | ✅ | ✅ | ❌ |
| HTTP/2 | ✅ | ❌ | ✅ | ✅ | ✅ |
| httpx-like API | ✅ | ❌ | ✅ | ❌ | ❌ |
| Cookie clear() works | ✅ | ✅ | ✅ | ✅ | ❌ |
| No memory leaks | ✅ | ✅ | ✅ | ✅ | ❌ |
| Auto-download binary | ✅ | N/A | N/A | ✅ | ❌ |
tlshttp wraps the bogdanfinn/tls-client Go library via ctypes. On first use, it automatically downloads the appropriate binary for your platform from the tls-client GitHub releases.
The Go library provides:
- Accurate browser TLS fingerprints (JA3, JA4, HTTP/2 settings, header order)
- HTTP/2 and HTTP/3 support
- Connection pooling
tlshttp adds:
- Pythonic httpx-like API
- Proper memory management (context managers, weak reference finalizers)
- Case-insensitive headers (RFC 7230)
- True async support via anyio thread pool
- Bug fixes for issues in other wrappers
| Platform | Architecture | Supported |
|---|---|---|
| Linux | x86_64 | ✅ |
| Linux | ARM64 | ✅ |
| Linux (Alpine/musl) | x86_64 | ✅ |
| macOS | x86_64 (Intel) | ✅ |
| macOS | ARM64 (M1/M2/M3) | ✅ |
| Windows | x86_64 | ✅ |
| Windows | x86 (32-bit) | ✅ |
# Clone
git clone https://github.com/Sekinal/tlshttp.git
cd tlshttp
# Install with dev dependencies
uv sync --group dev
# Run all tests
uv run pytest
# Skip integration tests (no network needed)
uv run pytest -m "not integration"
# Run specific test file
uv run pytest tests/test_headers.py -v
# Run with coverage
uv run pytest --cov=tlshttpMIT License - see LICENSE for details.
This project uses binaries from tls-client which is licensed under BSD-4-Clause.
- bogdanfinn/tls-client - The Go TLS client library
- httpx - API design inspiration