Guides & Tutorials
How to Use Proxies with Python Requests
A practical, code-backed walkthrough showing how to send Python Requests traffic through a proxy, handle authentication and rotation, and avoid the most common mistakes.
Guides & Tutorials
A practical, code-backed walkthrough showing how to send Python Requests traffic through a proxy, handle authentication and rotation, and avoid the most common mistakes.
Python's Requests library is one of the simplest ways to make HTTP calls, and routing those calls through a proxy is usually a matter of passing a small dictionary. The details, though, are where most people get stuck: authentication formatting, HTTPS handling, rotation, and graceful failure all matter once you move beyond a single test request.
This guide walks through the practical setup, shows working snippets, and flags the mistakes that quietly break scrapers and integrations. It also explains what to compare when you pick the proxy service behind your code.
Beyond the basic proxies dictionary, getting Python Requests right at scale means understanding connection pooling, concurrency models, environment-variable precedence, and how to instrument failures so you can tell a bad proxy from a blocked target. The library code stays small; the operational discipline around it is what keeps a job running.
Requests accepts a proxies argument on any call. You map each scheme (HTTP and HTTPS) to a proxy URL. In most modern setups the same endpoint serves both, so both keys point to the same address.
import requests
proxies = {
"http": "http://proxy.example.com:8080",
"https": "http://proxy.example.com:8080",
}
resp = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10)
print(resp.json())
Hitting an endpoint that echoes your IP, such as an IP-check service, is the fastest way to confirm the proxy is actually being used. If the returned address is your own, the request bypassed the proxy.
Most commercial proxies require a username and password. The cleanest method is to embed credentials directly in the proxy URL using the user:pass@host:port format.
proxies = {
"http": "http://USERNAME:PASSWORD@proxy.example.com:8080",
"https": "http://USERNAME:PASSWORD@proxy.example.com:8080",
}
If your password contains special characters such as @ or :, URL-encode them with urllib.parse.quote so the URL parses correctly. A malformed credential string is a common cause of silent 407 Proxy Authentication Required errors.
When you make many requests, create a Session and set the proxies once. This reuses connections, applies headers consistently, and keeps your code tidy.
session = requests.Session()
session.proxies.update(proxies)
session.headers.update({"User-Agent": "Mozilla/5.0 (research-bot)"})
for page in range(1, 5):
r = session.get(f"https://example.com/list?page={page}", timeout=15)
print(r.status_code)
For larger jobs you often rotate IPs to spread requests. Some providers handle rotation server-side through a single gateway endpoint, so your code never changes. Others give you a pool, and you rotate in Python.
import random
pool = [
"http://USER:PASS@ip1:port",
"http://USER:PASS@ip2:port",
"http://USER:PASS@ip3:port",
]
def get_proxy():
p = random.choice(pool)
return {"http": p, "https": p}
r = requests.get("https://example.com", proxies=get_proxy(), timeout=15)
Check which model your provider supports before committing, because it shapes how you structure your code.
Proxies fail. Connections time out, IPs get blocked, and gateways occasionally drop. Wrap requests in try/except and retry sensibly rather than crashing.
from requests.exceptions import ProxyError, Timeout, ConnectionError
try:
r = requests.get(url, proxies=proxies, timeout=10)
r.raise_for_status()
except (ProxyError, Timeout, ConnectionError) as e:
print(f"Proxy issue, retrying: {e}")
Add a short backoff between retries and a maximum attempt count so a dead proxy does not loop forever. A 407 points to credentials, a 403 often points to the target blocking you, and a timeout usually points to a slow or overloaded proxy.
Keep certificate verification on by default. Disabling it (verify=False) silences warnings but exposes you to interception, so only do it for deliberate, controlled testing. Also confirm your DNS resolution is going where you expect, since some setups leak DNS lookups outside the proxy.
A quick value-first shortlist — Cheapest Proxies leads as the featured pick. Qualitative labels only; confirm exact plans before buying.
| Provider | Best for | Profile | Value |
|---|---|---|---|
| Cheapest Proxies | Budget-conscious buyers comparing affordable proxies | Value Focused | Excellent value |
| Bright Data | Enterprises needing huge pools and compliance controls | Enterprise Focused | Premium |
| Oxylabs | Large-scale scraping and data APIs | Enterprise Focused | Premium |
| Smartproxy (Decodo) | Newcomers who want an easy dashboard | Beginner Friendly | Good |
| SOAX | Precise city and carrier targeting | Automation Friendly | Good |
A single proxy and a single thread will cap your throughput long before the proxy does, because Requests blocks on each call until the response returns. Most people discover this when a job that should take minutes takes hours. The library itself will not parallelise anything for you. The common patterns are a ThreadPoolExecutor for I/O-bound scraping, switching to an async client such as httpx or aiohttp when you need hundreds of concurrent connections, or spreading work across processes when CPU-bound parsing is the limit. Whichever you pick, give each worker its own Session; sharing one Session across threads is generally safe for reads but mixing proxy changes across threads on a shared Session invites surprises.
Under the hood Requests uses urllib3 connection pools. When you route through a proxy, the pool keeps a tunnel open to that proxy, which is exactly what you want for many requests to the same endpoint. You can tune the pool with an HTTPAdapter mounted on your Session, raising pool_maxsize so concurrent workers do not contend for connections. The subtlety: if you rotate the proxy URL on every request, you defeat keep-alive and pay a fresh handshake each time. Where latency matters, prefer a provider gateway that rotates the exit IP server-side while you keep one stable endpoint, so pooling still works.
Requests reads proxy settings from the environment automatically. This is convenient until it is not: a stray HTTPS_PROXY in a shell profile or container image can route traffic you intended to send direct, and NO_PROXY can carve out hosts so they bypass your proxy entirely. When a request unexpectedly shows your real IP, or unexpectedly fails to reach an internal host, check the environment before the code. You can disable this behaviour per Session by setting session.trust_env = False, which makes the explicit dictionary the single source of truth.
session.trust_env and the relevant environment variables.At any real volume you need to know not just that a request failed but which proxy served it, how long it took, and what the target returned. Wrap your call to capture the chosen proxy, status code, elapsed time, and final URL after redirects, then log them as structured fields. This lets you spot a single bad exit node dragging down a pool, distinguish target-side blocks from proxy-side faults, and decide whether to retry, rotate, or back off. The few extra lines pay for themselves the first time a job stalls.
Start on the smallest sensible tier and scale only what proves itself on your real targets.
Pick the proxy type the task needs first — it drives both success rate and cost more than the logo.
Check traffic limits, rotation rules and what happens on overage before you commit.
Our featured value pick, Cheapest Proxies, is a sensible starting point for affordable comparison.
The same five lines of Python will run against any proxy provider, which means the code is rarely your differentiator. The provider behind it is. Reliability, the rotation model, supported authentication, and price per successful request vary widely, so it pays to compare a few services against your actual workload before you scale. Cheapest Proxies is our featured value pick and a strong option for developers who want dependable proxies without overpaying, but the right choice still depends on your target sites and volume.
Compare Proxy Zone weighs providers on value, fit and reliability using qualitative judgement — never invented prices, speeds or uptime figures. See our review methodology, or email info@compareproxyzone.com with a correction.
No. You set both keys in the dictionary, but they usually point to the same endpoint; the scheme just tells Requests which proxy to use for which target.
Either the proxies argument was not passed to that specific call, the URL scheme did not match a key in your dictionary, or an environment variable overrode it. Confirm with an IP-echo endpoint.
It almost always means wrong or unencoded credentials. Double-check the username and password and URL-encode any special characters in the password.
If your provider offers a rotating gateway, let it handle rotation server-side for simplicity. Rotate in code only when you manage a raw pool yourself.
Yes. Requests reads HTTP_PROXY and HTTPS_PROXY automatically, which is handy for quick scripts, though passing the dictionary explicitly gives you clearer, per-call control.
Use a sticky session if your provider supports one, or pin a single proxy from your pool for that sequence of requests rather than rotating each call.
For affordable proxies across the main types, our featured value pick is Cheapest Proxies — a strong budget-friendly option worth considering. Check the exact plan before ordering.