Back to Blog
Guides
Ștefan RăcilăLast updated on May 7, 20269 min read

How to Use Proxies with Python Requests: From Basic to Production

How to Use Proxies with Python Requests: From Basic to Production
TL;DR: This guide walks through how to use proxies with Python Requests end to end: a working proxies dict, authenticated URLs, environment variables, Session reuse, SOCKS5 with no DNS leaks, and a rotation pool with retries and a circuit breaker. By the end, you will know when a managed API earns its keep over a DIY pool.

Introduction

If you have ever shipped a scraper that worked locally and then started returning 403s, 429s, or silent timeouts in production, you already know why proxies exist. Learning how to use proxies with Python Requests is the difference between a script that runs once on your laptop and a job that survives rate limits, geo-blocks, and IP bans across thousands of pages.

A Python Requests proxy setup, at its simplest, is a dictionary that maps http and https to a proxy URL and gets passed to requests.get(). That gets you unblocked for ten minutes. Production needs more: credentials kept out of git, sessions that persist cookies, SOCKS5 endpoints that do not leak DNS, retries with backoff, and a rotation strategy that does not keep hammering a dead proxy.

This guide is for intermediate Python developers who already know the basics of requests and now need a reliable path to add proxy support without rewriting their scraper. We cover how to use proxies with Python Requests from the trivial dict to a production rotation loop, with trade-offs called out in plain language.

Quick start: a working Python Requests proxy in five minutes

Before we go deep on rotation and retries, here is the eight-line example that 90% of developers actually need when they look up how to use proxies with Python Requests. Drop it in a file, swap in any working proxy host:port, and run.

import requests

proxies = {
    "http":  "http://203.0.113.10:8080",
    "https": "http://203.0.113.10:8080",
}

resp = requests.get("https://api.ipify.org?format=json", proxies=proxies, timeout=10)
print(resp.json())

If the IP it prints is the proxy's address and not yours, your proxy is in the request path. The rest of this guide is about hardening this pattern.

Prerequisites: Python, pip, and a proxy you can hit

You need Python 3.8 or later (python --version), pip, and at least one usable proxy host:port. A virtual environment (python -m venv venv) keeps dependencies clean per project. Install Requests with pip install requests. The proxy can come from a free list, a paid pool, or a local Squid or Tor instance.

How to Use Proxies with Python Requests: the mental model

Before chasing code, it helps to know how Requests actually decides where to send traffic. The library routes each call through a proxy URL on a per-scheme basis: HTTP, HTTPS, and (with an extra package) SOCKS. Three sources can supply that URL, in this rough order of precedence: the proxies= argument on a single call, the session.proxies dict on a Session, and finally the HTTP_PROXY / HTTPS_PROXY environment variables. The exact precedence and lowercase-variant handling are documented in the Requests advanced usage docs; always confirm against your pinned version.

Set up a basic proxy with Python Requests

The basic setup is two steps: build a proxies dictionary, then send a verification request through it. The next two subsections walk through each step and the gotchas that show up on dead or misconfigured proxies.

Build the proxies dictionary for HTTP and HTTPS

In Python Requests, proxies are passed as a dict mapping schemes to a proxy URL. Always populate both keys, even when you only plan to hit HTTPS targets, because redirects can downgrade the scheme.

proxies = {
    "http":  "http://user:pass@proxy.example.com:8080",
    "https": "http://user:pass@proxy.example.com:8080",
}
requests.get(url, proxies=proxies, timeout=(5, 15))

The timeout=(connect, read) tuple is non-negotiable in production. Without it, a dead proxy hangs your worker.

Confirm the proxy is in the request path

Hit an IP echo endpoint and compare against your real IP. Two reliable ones are https://api.ipify.org?format=json and https://httpbin.org/ip.

print(requests.get("https://api.ipify.org?format=json", proxies=proxies, timeout=10).json())

If the returned address differs from your local IP, the proxy is working. If it matches, the proxy silently failed open.

Authenticate proxies and protect credentials

Most paid proxies are authenticated, and that is where how to use proxies with Python Requests gets messier. The next three subsections cover URL embedding, environment variables, and the three error codes you will see.

Embed a username and password in the proxy URL

The accepted format is http://user:pass@host:port. If your password contains @, :, %, or /, URL-encode it or Requests will misparse the URL and you will see 407 errors:

from urllib.parse import quote
user = quote("alice@corp")
pwd  = quote("p@ss:w/rd%1")
proxy_url = f"http://{user}:{pwd}@proxy.example.com:8080"

Never commit that string to git.

Move secrets into HTTP_PROXY, HTTPS_PROXY, and NO_PROXY

Requests automatically picks up HTTP_PROXY, HTTPS_PROXY, and NO_PROXY from the environment, and per the official docs it also honors lowercase variants on POSIX systems. That means you can keep credentials out of code entirely:

# Linux / macOS
export HTTPS_PROXY="http://user:pass@proxy.example.com:8080"
export NO_PROXY="localhost,127.0.0.1,.internal"
# Windows
setx HTTPS_PROXY "http://user:pass@proxy.example.com:8080"

This is the cleanest pattern for Docker images and CI runners, where secrets live in the environment and not the repo.

Diagnose 407, 401, and 403 proxy errors

When something is off, the status code tells you which layer is unhappy.

Status

Likely cause

One-line fix

407 Proxy Authentication Required

Missing or malformed proxy creds

URL-encode the password and re-test

401 Unauthorized

Wrong username or password

Rotate the credential and verify with curl -x

403 Forbidden

Target site blocked the proxy IP

Switch to another proxy or change geo

Check the proxy first, then the target.

Reuse settings with requests.Session for cookies and connection pooling

A Session is the right primitive once you make more than one call. It persists proxies, default headers, and cookies, and it keeps the underlying TCP connection alive so you do not pay for a new TLS handshake on every hit. Session is built into Requests, so there is nothing extra to install.

session = requests.Session()
session.proxies = proxies
session.headers.update({"User-Agent": "my-scraper/1.0"})

session.post("https://example.com/login", data={"u": "alice", "p": "secret"})
dashboard = session.get("https://example.com/dashboard")  # cookies persist
print(dashboard.status_code, len(dashboard.content))

The same session covers .text, .json(), and .content, so text, JSON, and binary downloads all flow through the same Python Requests session proxy without reconfiguration.

Use SOCKS5 proxies via requests[socks]

Requests does not speak SOCKS out of the box. Pull in PySocks with the socks extra:

pip install "requests[socks]"

Then use the socks5h:// scheme. The trailing h tells PySocks to resolve DNS through the proxy instead of locally, which is what you want when you do not trust your ISP's resolver or you are running through Tor.

proxies = {
    "http":  "socks5h://127.0.0.1:9050",  # Tor default
    "https": "socks5h://127.0.0.1:9050",
}
requests.get("https://check.torproject.org/", proxies=proxies, timeout=15)

Plain socks5:// resolves DNS locally and quietly leaks the hostnames you visit.

Rotate proxies to avoid bans and rate limits

A single IP gets rate-limited and eventually blocked. The real answer to how to use proxies with Python Requests at scale is rotation, and the next three subsections show patterns of increasing maturity.

Random rotation with a retry loop

The simplest pattern is random.choice over a list of proxies, wrapped in a retry loop:

import random, requests
from requests.exceptions import RequestException

PROXIES = [{"http": p, "https": p} for p in PROXY_URLS]

def fetch(url, attempts=4):
    for _ in range(attempts):
        proxy = random.choice(PROXIES)
        try:
            return requests.get(url, proxies=proxy, timeout=10)
        except RequestException:
            continue
    raise RuntimeError("all attempts failed")

It works, but pure randomness happily picks dead proxies repeatedly and ignores load.

Power-of-two-choices for smarter load balancing

A well-studied refinement is power-of-two-choices: for every request, draw two proxies at random and use the one currently handling fewer in-flight calls. The intuition, supported by load-balancing literature commonly attributed to Mitzenmacher's 2001 analysis, is that this dampens worst-case load far better than uniform random while staying cheap.

import random
LOAD = {p: 0 for p in PROXY_URLS}

def pick():
    a, b = random.sample(PROXY_URLS, 2)
    return a if LOAD[a] <= LOAD[b] else b

Increment LOAD[proxy] before the request and decrement after. Exact gains depend on pool size; benchmark before quoting numbers.

Add a circuit breaker so dead proxies stop wasting requests

Random and power-of-two both keep selecting a dead proxy until it succeeds. A circuit breaker fixes that. Track state per proxy: CLOSED (healthy), OPEN (skipped), and HALF_OPEN (probationary).

import time
state = {p: {"fail": 0, "open_until": 0} for p in PROXY_URLS}
MAX_FAILS, COOLDOWN = 3, 60

def usable(p):
    return time.time() >= state[p]["open_until"]

def record(p, ok):
    if ok:
        state[p]["fail"] = 0
    else:
        state[p]["fail"] += 1
        if state[p]["fail"] >= MAX_FAILS:
            state[p]["open_until"] = time.time() + COOLDOWN

After cooldown, give the proxy one probationary request before fully reinstating it.

Retry failed requests with HTTPAdapter and urllib3 Retry

Mounting an HTTPAdapter with a urllib3 Retry policy onto a Session applies retries to every HTTP and HTTPS call from that session. Pin urllib3 (e.g., urllib3==2.2.*) so the parameter names stay stable across upgrades.

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

retry = Retry(
    total=3,
    status_forcelist=[429, 500, 502, 503, 504],
    backoff_factor=2,
    allowed_methods=["GET", "POST"],
    respect_retry_after_header=True,
)
adapter = HTTPAdapter(max_retries=retry)
s = Session()
s.mount("http://", adapter)
s.mount("https://", adapter)

With backoff_factor=2, urllib3 sleeps roughly backoff_factor * (2 ** (n - 1)) seconds between attempts (about 2, 4, 8 s). Combine retries with rotation so each retry also picks a new proxy.

Handle SSL verification and self-signed proxy certificates

If a proxy presents a self-signed cert, verify=False silences the warning but opens you to man-in-the-middle attacks, so use it only on trusted local proxies or in tests. The safer fix is to add the proxy or corporate CA bundle to the trust store via verify="/path/to/ca.pem" or REQUESTS_CA_BUNDLE. Suppress InsecureRequestWarning only after you have made the security trade-off deliberately.

When to ditch the DIY proxy pool for a managed scraping API

Run this checklist. If you tick three or more, a managed proxy or scraping API is usually cheaper than your time:

  • You need geo-targeting in more than two countries.
  • Bans cost real revenue, not just a retry.
  • Targets render content with JavaScript.
  • A senior engineer spends a day a week babysitting the pool.
  • Compliance demands audited residential IPs.

Key Takeaways

  • The shortest answer to how to use proxies with Python Requests is a dict mapping http and https to a proxy URL, passed via proxies= with a timeout.
  • Keep credentials out of source: prefer HTTP_PROXY, HTTPS_PROXY, and NO_PROXY env vars, and URL-encode special characters in passwords.
  • A requests.Session persists proxies, headers, and cookies and reuses TCP connections, which is the right default for any multi-call workflow.
  • Production rotation pairs power-of-two-choices with a circuit breaker and an HTTPAdapter Retry policy that survives 429s and 5xx.
  • For SOCKS5, install requests[socks] and use socks5h:// so DNS resolves through the proxy instead of leaking locally.

FAQ

Does Python Requests support SOCKS5 proxies out of the box?

No. The base requests install only ships HTTP and HTTPS support. Run pip install "requests[socks]" to pull in PySocks, then use a socks5:// or, preferably, socks5h:// URL in your proxies dict. That is the cleanest path to SOCKS support.

Why do my proxied requests still leak my real IP through DNS lookups?

Because the socks5:// scheme tells PySocks to resolve hostnames locally before tunneling the connection. Switch to socks5h://, where the trailing h means remote hostname resolution, so DNS queries travel through the SOCKS server. This matters most for Tor or any threat model where your DNS resolver is untrusted or logged.

How do I URL-encode a proxy password that contains @, :, or % characters?

Use urllib.parse.quote from the standard library: quote("p@ss:w/rd%1") becomes p%40ss%3Aw%2Frd%251. Embed the encoded value in http://user:encoded_pwd@host:port. Without encoding, those characters terminate the user-info segment early, and you will see a 407 Proxy Authentication Required even when the password is technically correct.

How do I tell Python Requests to skip the proxy for localhost or internal domains?

Set NO_PROXY to a comma-separated list of hosts or domain suffixes, for example NO_PROXY="localhost,127.0.0.1,.internal,.svc.cluster.local". Requests honors uppercase and lowercase variants on POSIX systems. For per-call overrides, pass proxies={"http": None, "https": None} to bypass any session-level proxy.

When should I move from a DIY rotating proxy pool to a managed scraping API?

When operational cost outweighs the bill. Concrete triggers: bans cost more than a retry, you need residential IPs in several countries, targets are JavaScript-heavy, or you spend more than a few engineering hours a week tuning the pool. Below that, a small DIY pool with retries and a circuit breaker is usually fine.

Conclusion

Knowing how to use proxies with Python Requests is less about a single trick and more about layering: a clean proxies dict to start, environment-variable credentials so secrets stay out of git, a Session for connection reuse and cookies, socks5h:// when DNS leaks matter, and rotation plus retries when one IP is no longer enough. Pair power-of-two-choices with a circuit breaker and an HTTPAdapter Retry policy, and your scraper stops collapsing the moment a proxy goes dark or a target throws 429s.

At some point, every team hits the line where running the pool costs more than the data is worth. If your targets are heavily protected, geo-specific, or JavaScript-rendered, a managed option like the WebScrapingAPI Scraper API handles the request layer, rotation, and unblocking behind a single endpoint, so you keep the parsing code you already wrote and just swap out the fetch step. Use the checklist above to decide; if three or more boxes are ticked, the math favors managed infrastructure over another sprint of pool maintenance. Either way, the patterns in this guide should keep your requests-based code healthy from prototype to production.

About the Author
Ștefan Răcilă, Full Stack Developer @ WebScrapingAPI
Ștefan RăcilăFull Stack Developer

Stefan Racila is a DevOps and Full Stack Engineer at WebScrapingAPI, building product features and maintaining the infrastructure that keeps the platform reliable.

Start Building

Ready to Scale Your Data Collection?

Join 2,000+ companies using WebScrapingAPI to extract web data at enterprise scale with zero infrastructure overhead.