HTTPX is the modern replacement for requests, with the same friendly API plus async, HTTP/2, and stricter defaults. Proxies work cleanly once you know the current API, and that is exactly the problem: HTTPX renamed its proxy arguments, and the version that finally removed the old names landed in late 2024, so most of the tutorials and Stack Overflow answers you will find are now wrong. This guide is the current reference: the proxy argument, mounts for conditional routing, async clients, SOCKS5, authentication, and verifying the proxy actually carries your traffic.
We run a proxy network and a live proxy checker, so we see the fallout of that rename constantly: code copied from a 2023 tutorial that dies on unexpected keyword argument 'proxies'. Every example below is written for current HTTPX and is copy-paste runnable. Where one needs a live proxy, pull a fresh one from our free proxy API, which returns real, recently checked endpoints without a key.
How do you use a proxy with HTTPX?
Pass a single proxy URL to the client's proxy argument: httpx.Client(proxy="http://user:pass@host:port"). Every request that client makes goes through that proxy. For different proxies per scheme, domain, or port, use mounts with httpx.HTTPTransport. The identical proxy argument works on httpx.AsyncClient, and SOCKS5 needs the httpx[socks] extra.
The one-proxy case, and the rename that broke your tutorials
For a single proxy, the whole thing is one argument:
import httpx
with httpx.Client(proxy="http://user:[email protected]:8080") as client:
r = client.get("https://httpbin.org/ip")
print(r.json())
If you have seen proxies= in older code, here is the timeline that matters. Per the HTTPX changelog, version 0.26.0 (20 December 2023) added the singular proxy argument and deprecated proxies. Version 0.28.0 (28 November 2024) removed proxies entirely. So today:
proxies="http://..."becomesproxy="http://...".- A per-scheme
proxies={"http://": ..., "https://": ...}dict becomes amountsdict, covered next.
That is the entire migration. If your only proxy need is one endpoint for everything, proxy= is all you will ever touch.
Per-scheme and per-host routing with mounts
When you need more than one proxy, or a proxy for some hosts and none for others, HTTPX routes through transports you mount on URL patterns. Each pattern maps to an httpx.HTTPTransport carrying its own proxy:
import httpx
proxy_mounts = {
"http://": httpx.HTTPTransport(proxy="http://203.0.113.7:8080"),
"https://": httpx.HTTPTransport(proxy="http://203.0.113.7:8080"),
}
with httpx.Client(mounts=proxy_mounts) as client:
r = client.get("https://httpbin.org/ip")
print(r.json())
One detail from HTTPX's own docs is worth repeating: the proxy URL for the https:// key should still use the http:// scheme, because most proxies accept HTTP connections only and tunnel your HTTPS through CONNECT. The mismatch looks wrong and is correct.
Mounts get more powerful because the patterns can be host-scoped, and HTTPX matches from most specific to least specific. You can send everything through one proxy, a subdomain group through another, and bypass a host entirely by mounting None:
mounts = {
"all://": httpx.HTTPTransport(proxy="http://203.0.113.7:8080"), # default
"all://*.internal.example": None, # no proxy here
"all://*.scrape-target.com": httpx.HTTPTransport(proxy="http://198.51.100.14:8080"),
}
with httpx.Client(mounts=mounts) as client:
...
all:// matches every scheme, all://*.example.com matches subdomains, and a None value turns proxying off for anything that matches that pattern. This is the HTTPX equivalent of the scheme-plus-host keys people used in the requests proxies dict, and it is strictly more expressive.
Authentication
Put the credentials in the proxy URL userinfo, user:pass@host:port, exactly as with requests and curl:
import httpx
with httpx.Client(proxy="http://myuser:[email protected]:8080") as client:
r = client.get("https://httpbin.org/ip")
print(r.json())
If the password contains URL-special characters (@, :, /, #), percent-encode it first with urllib.parse.quote(password, safe=""), or the URL parses wrong. Wrong or missing credentials come back as HTTP 407 Proxy Authentication Required, which is good news in one sense: it proves the proxy is alive and reachable, and only the credentials are the problem. We have a focused fix in how to fix 407 Proxy Authentication Required.
Async clients
The reason many people choose HTTPX is AsyncClient, and proxies work there with no new concepts. The same proxy argument, awaited:
import asyncio
import httpx
async def main():
async with httpx.AsyncClient(proxy="http://user:[email protected]:8080") as client:
r = await client.get("https://httpbin.org/ip")
print(r.json())
asyncio.run(main())
If you need async mounts, the only change is the transport class: use httpx.AsyncHTTPTransport(proxy=...) inside the mounts dict instead of the sync HTTPTransport. Everything else, the patterns, the None bypass, the auth, is identical to the sync version.
SOCKS5 proxies
HTTPX speaks HTTP proxies out of the box and needs one extra for SOCKS:
pip install httpx[socks]
That pulls in the socksio dependency. Then a socks5:// URL goes straight into proxy:
import httpx
with httpx.Client(proxy="socks5://user:[email protected]:1080") as client:
r = client.get("https://httpbin.org/ip")
print(r.json())
Without the extra installed, a socks5:// URL raises an ImportError pointing you at exactly this install. For why SOCKS5 (and its remote-DNS socks5h cousin) matters for privacy, see what is a SOCKS5 proxy.
Environment variables
HTTPX trusts the environment by default, following the same convention as curl and requests. Per its documentation, it reads HTTP_PROXY, HTTPS_PROXY and ALL_PROXY for the proxy, and NO_PROXY for a comma-separated bypass list. That is convenient until a variable set in your shell or CI proxies you silently. Two ways to take back control:
# Ignore the environment entirely for this client
client = httpx.Client(trust_env=False)
# Or override it with an explicit proxy
client = httpx.Client(proxy="http://203.0.113.7:8080")
For a proxy you cannot afford to have overridden, set it explicitly on the client rather than relying on the environment.
Rotating a pool
Because the proxy is fixed when the client is built, the simplest rotation builds a fresh client per attempt and retries on a different proxy when one fails:
import random
import httpx
POOL = [
"http://203.0.113.7:8080",
"http://203.0.113.24:3128",
"http://198.51.100.14:8080",
]
def get_rotating(url, tries=4):
for _ in range(tries):
proxy = random.choice(POOL)
try:
with httpx.Client(proxy=proxy, timeout=15.0) as client:
r = client.get(url)
r.raise_for_status()
return r
except httpx.HTTPError:
continue # dead or blocked exit, try the next one
raise RuntimeError(f"all {tries} proxies failed for {url}")
html = get_rotating("https://example.com").text
Catching httpx.HTTPError sweeps up both transport failures (httpx.ConnectError, httpx.ConnectTimeout, httpx.ProxyError, httpx.ReadTimeout) and the status errors raised by raise_for_status. One efficiency note: a fresh client per request throws away connection pooling, so for heavy rotation keep a dict of clients keyed by proxy, reuse them, and close them at the end. You do not have to hand-maintain the list either. Our free proxy API returns a fresh pool as plain text you can split straight into POOL:
POOL = httpx.get(
"https://hproxy.com/api/proxy-list",
params={"format": "txt", "protocol": "http", "recent": "true", "limit": 50},
timeout=15.0,
).text.split()
POOL = [f"http://{p}" for p in POOL]
For production, a rotating gateway that gives you one endpoint and a fresh residential IP per request removes list management entirely, but rotating a list by hand is the best way to understand what that gateway does for you.
Timeouts are already on, but tune them
A pleasant difference from requests: HTTPX applies a default 5-second timeout to every operation, so a silent proxy cannot hang your program the way an untimed requests call would. You do not have to remember to add one, but you should tune it for proxied work, where the connect and read phases have very different budgets:
timeout = httpx.Timeout(connect=5.0, read=30.0, write=10.0, pool=5.0)
with httpx.Client(proxy="http://203.0.113.7:8080", timeout=timeout) as client:
r = client.get("https://example.com")
The connect=5.0 caps how long to wait for the proxy to answer; read=30.0 caps how long to wait for data once connected. A slow or overloaded proxy trips the read budget, an unreachable one trips the connect budget.
Verify the exit IP
Never assume a proxy works. Confirm the target sees the proxy's address and not yours before you trust an exit with real traffic:
import httpx
proxy = "http://203.0.113.7:8080"
real = httpx.get("https://httpbin.org/ip", timeout=10.0).json()["origin"]
via = httpx.get("https://httpbin.org/ip", proxy=proxy, timeout=15.0).json()["origin"]
print("real: ", real)
print("proxy:", via)
assert real != via, "proxy is not changing your IP"
If the two IPs match, the request never went through the proxy. It is also worth checking what headers arrive at the target, because a transparent proxy can forward your real address in X-Forwarded-For even while origin looks changed. Our proxy checker runs that whole battery, exit IP, anonymity grade, real geolocation, and latency, in one paste if you would rather not script it.
Where to go from here
HTTPX makes the proxy side easy once you are on the current API, which frees you to spend your attention on the two things a client library cannot do for you: verifying each exit before you trust it, and pulling from a pool that is actually alive. For a live pool to test against, the free proxy list re-verifies its entries every few minutes.
From here, the Python requests guide is the direct sibling to this one if you maintain code on both clients, the cURL guide is the fastest shell-side sanity check for a proxy, and proxies for web scraping covers choosing the right proxy type and the request hygiene that no client can add for you. When a project graduates to production, get IPs nobody else is burning on our paid pools.
Sources
- HTTPX changelog: the
proxyargument added andproxiesdeprecated in 0.26.0 (December 2023), andproxiesremoved in 0.28.0 (November 2024). - HTTPX: Transports and routing: the
mountspatterns, includingall://, host wildcards, and mapping a pattern toNoneto bypass a proxy. - HTTPX: Environment variables:
HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXYhandling and thetrust_envswitch.