Back to blog

Guide

Python Requests With a Proxy: Setup, Auth, SOCKS5 and Rotation

How to use a proxy with Python requests: the proxies dict, authentication, SOCKS5 support, sessions, retries, rotation, and the environment-variable behaviour that catches people out.

Using a proxy with Python requests is one dictionary: map each target scheme to a proxy URL and pass it as proxies=. The part that catches people out is that the dictionary keys are the scheme of the target URL, not of the proxy — so a dict containing only http does nothing at all for an https:// request.

import requests
 
proxies = {
    "http": "http://user:pass@gateway.example.com:8000",
    "https": "http://user:pass@gateway.example.com:8000",
}
 
r = requests.get("https://api.ipify.org", proxies=proxies, timeout=10)
print(r.text)   # should print the proxy's IP, not yours

Note that both values are http:// even though one key is https. That is correct: the proxy is reached over HTTP, then CONNECT tunnels the TLS session through it. Writing https:// as the proxy scheme means "speak TLS to the proxy itself", which is a different thing and usually not what you have.

Python Requests With a Proxy

Authentication

Credentials go in the URL, and they must be URL-encoded:

from urllib.parse import quote
 
user = quote("my-username", safe="")
pw   = quote("p@ss:word/!", safe="")   # special chars would break the URL
 
proxies = {
    "http":  f"http://{user}:{pw}@gateway.example.com:8000",
    "https": f"http://{user}:{pw}@gateway.example.com:8000",
}

Skipping quote() is a real and silent failure mode. An @ or : inside a password splits the URL in the wrong place, and what you get back is a confusing connection error rather than an auth error.

Keep credentials out of source:

import os
 
user = quote(os.environ["PROXY_USER"], safe="")
pw   = quote(os.environ["PROXY_PASS"], safe="")

If your provider supports IP allowlisting, that avoids credential handling entirely — see proxy authentication methods.

Sessions: use them

For more than one request, use a Session. It reuses the underlying TCP connection, which is a large speed difference over a proxy because you skip the handshake every time.

import requests
 
session = requests.Session()
session.proxies.update({
    "http":  "http://user:pass@gateway.example.com:8000",
    "https": "http://user:pass@gateway.example.com:8000",
})
 
for url in urls:
    r = session.get(url, timeout=(5, 15))
    print(r.status_code)

One nuance worth knowing: connection reuse means a rotating gateway may keep giving you the same exit IP for the life of the connection, because rotation usually happens per connection rather than per request. If you need a fresh IP each time, either use separate requests without a session or check whether your provider offers per-request rotation on the endpoint.

SOCKS5, and why socks5h matters

SOCKS5 needs an extra dependency:

pip install "requests[socks]"

Then:

proxies = {
    "http":  "socks5h://user:pass@gateway.example.com:1080",
    "https": "socks5h://user:pass@gateway.example.com:1080",
}

Use socks5h://, not socks5://. The difference:

SchemeWho resolves DNSConsequence
socks5://Your machineYour resolver sees every hostname you request
socks5h://The proxyHostnames resolve on the proxy's network

Two reasons to prefer the h form: your DNS queries stop leaking the list of hosts you are visiting to your local resolver, and it works for hostnames that only resolve correctly from the proxy's location — which is exactly the case when you are targeting region-specific infrastructure. The same distinction exists in cURL.

Environment variables

requests reads HTTP_PROXY, HTTPS_PROXY, ALL_PROXY and NO_PROXY from the environment by default. That is convenient, and it is also a source of confusion when something proxies unexpectedly.

session = requests.Session()
session.trust_env = False   # ignore environment proxy settings entirely

Set trust_env = False when you want your explicit configuration to be the only thing in play. This is worth doing in library code, where an ambient environment variable on someone else's machine will change your behaviour.

To exclude specific hosts:

export NO_PROXY="localhost,127.0.0.1,.internal.example.com"

Timeouts and retries

Never run a proxied request without a timeout. The default is to wait forever, and a dead proxy will hang your process.

# (connect timeout, read timeout)
r = session.get(url, timeout=(5, 20))

For retries with backoff, mount an adapter rather than writing a loop:

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
 
retry = Retry(
    total=3,
    backoff_factor=1,              # 1s, 2s, 4s
    status_forcelist=[429, 500, 502, 503, 504],
    allowed_methods=["GET", "POST"],
)
 
session = requests.Session()
adapter = HTTPAdapter(max_retries=retry, pool_maxsize=50)
session.mount("http://", adapter)
session.mount("https://", adapter)

pool_maxsize matters if you are running concurrent requests — the default connection pool is small, and requests beyond it queue silently, which looks like the proxy being slow.

Rotation

Two approaches.

Gateway rotation (recommended). Point every request at one endpoint and let the provider assign a fresh exit per connection. No rotation code:

proxies = {
    "http":  "http://user:pass@gateway.example.com:8000",
    "https": "http://user:pass@gateway.example.com:8000",
}
 
for url in urls:
    r = requests.get(url, proxies=proxies, timeout=10)

Manual rotation. Only worth it when you need explicit control over which IP handles which request:

import itertools, requests
 
proxy_list = [
    "http://user:pass@proxy1.example.com:8000",
    "http://user:pass@proxy2.example.com:8000",
    "http://user:pass@proxy3.example.com:8000",
]
pool = itertools.cycle(proxy_list)
 
def fetch(url):
    p = next(pool)
    return requests.get(url, proxies={"http": p, "https": p}, timeout=10)

Gateway rotation is usually better because the provider removes dead exits from the pool for you. A hand-managed list degrades as IPs go offline, and you find out through failures. See proxy rotation strategies for when to rotate per request versus holding a session.

Sticky sessions

When a sequence of requests has to look like one visitor — login, cart, multi-step form — you need the same IP throughout. Most providers encode this in the username:

import uuid
 
session_id = uuid.uuid4().hex[:8]
proxy = f"http://user-session-{session_id}:pass@gateway.example.com:8000"
 
s = requests.Session()
s.proxies.update({"http": proxy, "https": proxy})
# every request on this session now exits from one IP

The exact parameter format is provider-specific — check your dashboard. How to maintain sticky sessions covers the concept.

Verifying it works

import requests
 
direct  = requests.get("https://api.ipify.org", timeout=10).text
proxied = requests.get("https://api.ipify.org", proxies=proxies, timeout=10).text
 
print(f"direct:  {direct}")
print(f"proxied: {proxied}")
assert direct != proxied, "proxy is not being used"

That assertion is worth keeping in your test suite. Silent proxy bypass is common and produces no error.

Common errors

ProxyError / Failed to establish a new connection — wrong host or port, proxy down, or a firewall blocking the port.

407 Proxy Authentication Required — bad credentials, or unencoded special characters in the password. Wrap both in quote().

SSLError — usually a proxy that terminates TLS with its own certificate. Point verify at its CA rather than disabling verification:

r = session.get(url, proxies=proxies, verify="/path/to/proxy-ca.pem")

Works direct, 403 through the proxy — the target is blocking the proxy's IP range. This is the normal outcome for datacenter ranges on protected sites; residential IPs are the usual fix. See why proxies get blocked.

Blocked even on good residential IPs — the IP is probably not the failing signal. requests has a distinctive TLS fingerprint that identifies it as a scripting library before any header is read. For hardened targets you need a real browser engine — see Playwright — and anti-bot detection in 2026 explains why.

Same IP on every request despite a rotating endpoint — connection reuse in a Session. Rotation is typically per connection.

A pattern that holds up

import os
from urllib.parse import quote
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
 
def build_session() -> requests.Session:
    user = quote(os.environ["PROXY_USER"], safe="")
    pw   = quote(os.environ["PROXY_PASS"], safe="")
    proxy = f"http://{user}:{pw}@gateway.example.com:8000"
 
    s = requests.Session()
    s.trust_env = False
    s.proxies.update({"http": proxy, "https": proxy})
    s.headers.update({"Accept-Language": "en-US,en;q=0.9"})
 
    retry = Retry(
        total=3,
        backoff_factor=1,
        status_forcelist=[429, 500, 502, 503, 504],
    )
    adapter = HTTPAdapter(max_retries=retry, pool_maxsize=50)
    s.mount("http://", adapter)
    s.mount("https://", adapter)
    return s

Credentials from the environment, explicit configuration only, retries on transient failures, and a connection pool sized for concurrency.

FlameProxies supports HTTP(S) and SOCKS5 across 80M-plus residential IPs in 180-plus countries at $0.50/GB, dropping to $0.45/GB above 1TB, with unlimited concurrent sessions — current rates are on the pricing page. For broader guidance on choosing between residential and datacenter for scripted collection, see best proxies for web scraping.

Frequently asked questions

How do I use a proxy with Python requests?
Pass a dictionary mapping each scheme to a proxy URL: proxies = {'http': 'http://user:pass@host:port', 'https': 'http://user:pass@host:port'}, then requests.get(url, proxies=proxies). The keys are the scheme of the target URL, not the scheme of the proxy, which is the most common point of confusion.
Why does requests ignore my proxies dict?
Usually because the keys do not match the target URL's scheme. A proxies dict containing only 'http' does nothing for an https:// request. Set both keys. If the dict looks right, check whether a proxy environment variable is set — requests reads HTTP_PROXY and HTTPS_PROXY too, and you can disable that with session.trust_env = False.
Does Python requests support SOCKS5 proxies?
Yes, but you need the extra dependency: pip install requests[socks], which installs PySocks. Then use a socks5:// or socks5h:// URL. Prefer socks5h:// — the h makes the proxy resolve DNS, which stops your hostname lookups leaking to your local resolver.
How do I rotate proxies in Python requests?
Either point every request at a rotating gateway endpoint and let the provider assign a new exit IP per connection, which needs no code, or hold a list of proxies and pick one per request. The gateway approach is simpler and usually better, because the provider manages pool health for you.
Why am I getting 407 Proxy Authentication Required?
The proxy rejected your credentials or received none. Check the username and password, and make sure they are URL-encoded if they contain special characters — an @ or a colon in a password will break the proxy URL silently. If your provider uses IP allowlisting instead, confirm your current public address is on the list.
Should I use verify=False with a proxy?
No, not in anything that runs unattended. It disables TLS certificate verification, which removes exactly the protection that stops traffic interception. If your proxy terminates TLS with its own certificate, point requests at that CA with verify='/path/to/ca.pem' instead of switching verification off.