IPStation logoIPStation
All articles

How to Use Proxies with Python Requests (With Code Examples)

October 17, 2026·9 views·pythontutorialproxies
How to Use Proxies with Python Requests (With Code Examples)

Routing your Python traffic through a proxy is one of those tasks that looks trivial until it isn't. The requests library makes the basics a one-liner, but production use — authentication, rotation, sticky sessions, retries, timeouts — has real gotchas. This tutorial walks through all of it with complete, working code examples you can copy, paste, and run.

Prerequisites: Python 3.8+ and the requests library (pip install requests). All examples use https://httpbin.org/ip, a free endpoint that echoes back the IP address your request came from — perfect for verifying your proxy is actually working.

1. The Basic Proxy Setup

Every proxy configuration in requests starts with the same structure: a dictionary mapping the http and https keys to proxy URLs, passed via the proxies parameter.

import requests

proxies = {
    "http": "http://proxy.example.com:8080",
    "https": "http://proxy.example.com:8080",
}

response = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10)
print(response.json())
# {"origin": "203.0.113.45"}  <- the proxy's IP, not yours

Two things to note:

  • You almost always need both keys. The key name refers to the protocol of the target URL, not the proxy. If you only set http, your HTTPS requests will bypass the proxy entirely — the most common silent failure in proxy setups.
  • The proxy URL scheme is usually http:// even for HTTPS targets. That just means "talk to the proxy over plain HTTP"; the connection to the target site is still encrypted end-to-end via the CONNECT method.

2. Authenticated Proxies

Most paid proxies require a username and password. Embed them directly in the proxy URL using standard URL syntax:

import requests

proxies = {
    "http": "http://myuser:[email protected]:8080",
    "https": "http://myuser:[email protected]:8080",
}

response = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10)
print(response.json())

Behind the scenes, requests converts these credentials into a Proxy-Authorization header on the CONNECT request. If your password contains special characters (@, :, /, %), they will break the URL parsing and you'll get a 407 Proxy Authentication Required error even with correct credentials. URL-encode them first:

from urllib.parse import quote

username = "myuser"
password = quote("p@ss:w/rd%1", safe="")  # -> p%40ss%3Aw%2Frd%251

proxy_url = f"http://{username}:{password}@proxy.example.com:8080"
proxies = {"http": proxy_url, "https": proxy_url}

For proxies requiring digest authentication (common in enterprise environments), use HTTPProxyAuth explicitly:

from requests.auth import HTTPProxyAuth
import requests

proxies = {"http": "http://proxy.example.com:8080",
           "https": "http://proxy.example.com:8080"}
auth = HTTPProxyAuth("myuser", "mypassword")

response = requests.get("https://httpbin.org/ip", proxies=proxies, auth=auth, timeout=10)

Security note: never hard-code credentials in your source. Read them from environment variables or a secrets manager — leaked proxy credentials in a public repo are an invitation for someone else to burn through your bandwidth quota.

3. Using a Session

If you're making more than one request, use a requests.Session. It persists the proxy configuration (plus headers and cookies) across calls and reuses TCP connections, which is both cleaner and faster:

import requests

session = requests.Session()
session.proxies.update({
    "http": "http://myuser:[email protected]:8080",
    "https": "http://myuser:[email protected]:8080",
})
session.headers.update({"User-Agent": "MyScraper/1.0"})

for path in ["/ip", "/headers", "/user-agent"]:
    r = session.get(f"https://httpbin.org{path}", timeout=10)
    print(r.status_code, path)

One caveat: requests reads proxy settings from the HTTP_PROXY, HTTPS_PROXY, and NO_PROXY environment variables by default, and those override nothing — they merge with, and can surprise, your explicit dict. If your environment sets proxies you don't want, disable the behavior with session.trust_env = False.

4. Rotating Proxies Per Request

Hitting the same target repeatedly from one IP is the fastest way to get rate-limited. The simplest rotation pattern uses itertools.cycle to walk through a pool:

import requests
from itertools import cycle

proxy_pool = [
    "http://myuser:[email protected]:8080",
    "http://myuser:[email protected]:8080",
    "http://myuser:[email protected]:8080",
]
pool = cycle(proxy_pool)

urls = ["https://httpbin.org/ip"] * 6

for url in urls:
    proxy = next(pool)
    proxies = {"http": proxy, "https": proxy}
    try:
        r = requests.get(url, proxies=proxies, timeout=10)
        print(f"{proxy.split('@')[-1]:35s} -> {r.json()['origin']}")
    except requests.RequestException as e:
        print(f"{proxy} failed: {e}")

Many proxy providers also offer rotating gateway endpoints — a single hostname that assigns a fresh exit IP on every request automatically. If your provider supports this, it's far cleaner than maintaining your own pool: you keep one proxy URL in your code and the rotation happens server-side.

5. Sticky Sessions (Keeping the Same IP)

Rotation isn't always what you want. Logins, shopping carts, and multi-step workflows break if your IP changes mid-flow. Providers solve this with sticky sessions: you embed a session ID in the proxy username, and every request with that ID exits through the same IP (typically for 1–30 minutes).

import requests

# The "-session-abc123" suffix pins this connection to one exit IP.
# Format varies by provider; check your provider's docs.
proxies = {
    "http": "http://myuser-session-abc123:[email protected]:8080",
    "https": "http://myuser-session-abc123:[email protected]:8080",
}

session = requests.Session()
session.proxies.update(proxies)

# All of these requests exit through the same IP:
r1 = session.get("https://httpbin.org/ip", timeout=10)
r2 = session.get("https://httpbin.org/ip", timeout=10)
assert r1.json()["origin"] == r2.json()["origin"]
print("Sticky IP:", r1.json()["origin"])

Use a different session ID per account or workflow when you're managing multiple identities — that keeps each one's network footprint separate and consistent.

6. Timeouts: Always Set Them

A request without a timeout can hang forever on a dead proxy. Always pass one. A single number sets both the connect and read timeout; a tuple sets them separately:

import requests

proxies = {"http": "http://myuser:[email protected]:8080",
           "https": "http://myuser:[email protected]:8080"}

# Wait max 5s to connect, max 15s for the server to respond
r = requests.get("https://httpbin.org/delay/2", proxies=proxies, timeout=(5, 15))
print(r.status_code)

Sensible defaults for proxy work: a short connect timeout (3–5 s, since a healthy proxy connects fast) and a generous read timeout (15–30 s, since residential relays can be slow).

7. Retries and Error Handling

Proxies fail — connections drop, exit nodes go offline, targets return 429s. Wrap your calls with a retry policy using urllib3's Retry mounted on a session adapter:

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

retry_strategy = Retry(
    total=3,                       # max 3 retries
    backoff_factor=0.5,            # 0.5s, 1s, 2s between attempts
    status_forcelist=[429, 500, 502, 503, 504],
    allowed_methods=["GET", "POST"],
)
adapter = HTTPAdapter(max_retries=retry_strategy)

session = requests.Session()
session.proxies.update({
    "http": "http://myuser:[email protected]:8080",
    "https": "http://myuser:[email protected]:8080",
})
session.mount("http://", adapter)
session.mount("https://", adapter)

try:
    r = session.get("https://httpbin.org/status/503", timeout=10)
    print(r.status_code)
except requests.exceptions.ProxyError:
    print("Proxy connection failed — try a different exit node")
except requests.exceptions.ConnectTimeout:
    print("Proxy too slow — consider a shorter timeout or another IP")
except requests.exceptions.RequestException as e:
    print(f"Request failed: {e}")

Catch specific exceptions rather than a bare except: ProxyError means the proxy itself is unreachable or refused auth, ConnectTimeout means it's too slow, and SSLError often points to a corporate MITM proxy whose CA certificate you need to trust (point verify at the CA bundle — don't set verify=False in production).

8. Verify Your Exit IP

Before running any real workload, confirm the proxy is doing what you think. Compare your direct IP against the proxied one:

import requests

direct_ip = requests.get("https://httpbin.org/ip", timeout=10).json()["origin"]

proxies = {"http": "http://myuser:[email protected]:8080",
           "https": "http://myuser:[email protected]:8080"}
proxied_ip = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10).json()["origin"]

print(f"Direct:  {direct_ip}")
print(f"Proxied: {proxied_ip}")
assert direct_ip != proxied_ip, "Proxy is not being used!"

For a deeper check — geolocation, ASN type, and anonymity level — run the IP through a dedicated test tool rather than eyeballing it.

Bonus: SOCKS5 Proxies

Some providers issue SOCKS5 endpoints instead of HTTP. requests supports them via the requests[socks] extra, which installs PySocks:

pip install "requests[socks]"
import requests

proxies = {
    "http": "socks5h://myuser:[email protected]:1080",
    "https": "socks5h://myuser:[email protected]:1080",
}

r = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10)
print(r.json())

Use socks5h:// (with the trailing h), not socks5://. The h means hostnames are resolved remotely through the proxy; without it, DNS lookups happen on your local machine and leak every domain you visit — defeating much of the point.

Common Pitfalls Checklist

| Symptom | Cause | Fix | |---|---|---| | Proxy ignored on HTTPS sites | Only the http key is set | Set the https key too | | 407 Proxy Authentication Required | Special characters in the password | URL-encode credentials with urllib.parse.quote | | Environment proxy overrides your dict | HTTP_PROXY set in the environment | session.trust_env = False or unset the variable | | DNS leaks with SOCKS | Using socks5:// (local resolution) | Switch to socks5h:// | | SSL errors through a corporate proxy | MITM proxy certificate | Point verify at the CA bundle, never False | | Hanging requests | No timeout set | Always pass timeout= |

FAQ

Do I need to install anything extra for SOCKS proxies?

Yes. The base requests install only handles HTTP(S) proxies. Run pip install "requests[socks]" to pull in PySocks, then use socks5h:// URLs in your proxies dict. The h variant routes DNS through the proxy, which prevents local DNS leaks.

How do I rotate proxies without writing my own pool logic?

Two options. The DIY route is itertools.cycle over a list of proxy URLs, as shown above. The cleaner route is a provider-side rotating endpoint: many proxy services give you one gateway hostname that assigns a fresh exit IP per request automatically — no pool management, no health-checking dead nodes in your code.

Why am I getting a 407 error with the right password?

Almost always URL parsing. Characters like @, :, /, and % in passwords terminate the user-info segment of the proxy URL early, so the proxy sees truncated credentials. URL-encode the password with urllib.parse.quote(password, safe="") before embedding it.

Should I use a Session or plain requests.get for proxy work?

Use a Session for anything beyond a one-off call. It persists your proxy config, reuses TCP connections (faster), keeps cookies across requests (needed for logins and sticky workflows), and lets you mount a single retry adapter for all traffic. Plain requests.get is fine for quick tests.

Try it yourself

Grab a proxy plan that fits your workload on the IPStation pricing page — rotating residential for protected targets, datacenter for high-volume jobs — then paste your new credentials into the free proxy connection test to verify speed, location, and anonymity before wiring them into your script.

Put it into practice

Get clean residential and mobile proxies live in under two minutes — or test your current IP first.