Python Requests Proxy: Setup, Authentication, SOCKS5 and Rotation

The earlier version passed proxy credentials with auth=('user', 'password'). That parameter is sent to the website, not to the proxy, so an authenticating proxy answers 407. Credentials belong in the proxy URL, as shown below. Every example on this page was re-run on 2026-10-26 with requests 2.34.2 against a local proxy lab, and the page now covers environment variables, SOCKS5, rotation and proxy errors.
To send a request through a proxy with Python Requests, pass a proxies dictionary with an http and an https key. Put the proxy's username and password inside the proxy URL, percent-encoded. That is the whole answer; the rest of this page covers what each part means and the errors you get when one of them is wrong.
The short answer
from urllib.parse import quote
import requests
user, password = "lab", "p@ss#w0rd"
proxy = f"http://{quote(user, safe='')}:{quote(password, safe='')}@127.0.0.1:8899"
proxies = {"http": proxy, "https": proxy}
response = requests.get("http://127.0.0.1:8000/ip", proxies=proxies, timeout=10)
print(proxy)
print(response.status_code, response.json())
http://lab:p%40ss%23w0rd@127.0.0.1:8899
200 {'path': '/ip', 'via': 'auth-proxy-8899', 'authorization_sent_to_site': False}
The examples run against a local lab: an echo server on port 8000 and proxies on 8899 (with a username and password), 8901 and 8902. Each proxy adds an X-Lab-Proxy header to plain-HTTP requests, so the echo server's via field shows which proxy carried the request. Replace 127.0.0.1:8899 with your provider's host and port.
ScrapingAnt API
The same proxies dict, with rotation built in
Keep your Requests code and point the proxies dict at ScrapingAnt's proxy mode: requests rotate across exit IPs, so there is no pool to buy or rotate yourself, and the 5 requests below came from 5 different IPs. Pick any country from the documented list with proxy_country, at no extra charge, and add proxy_type=residential to the username for residential IPs, as the proxy mode docs show. There is a free plan to start.
import os
import requests
import urllib3
urllib3.disable_warnings() # proxy mode re-signs HTTPS, so these requests use verify=False; this silences the warning
API_KEY = os.environ["SCRAPINGANT_API_KEY"]
proxy = f"http://scrapingant&browser=false&proxy_country=FR:{API_KEY}@proxy.scrapingant.com:8080"
proxies = {"http": proxy, "https": proxy}
exit_ips = set()
for _ in range(5):
response = requests.get("https://ipinfo.io/json", proxies=proxies, verify=False, timeout=60)
exit_ip = response.json()
exit_ips.add(exit_ip["ip"])
print(response.status_code, exit_ip["country"], exit_ip["city"])
print(len(exit_ips), "different exit IPs in 5 requests")
200 FR Aubervilliers
200 FR Aubervilliers
200 FR Aubervilliers
200 FR Aubervilliers
200 FR Aubervilliers
5 different exit IPs in 5 requests
Run on 2026-10-26 with requests 2.34.2. The username carries the API parameters and the password is your API key. Proxy mode asks clients not to verify certificates because it re-signs HTTPS traffic, so use verify=False for this proxy only.
What the http and https keys mean
The keys in proxies are the scheme of the website you request, not the type of proxy. "https": proxy means "use this proxy for https:// URLs". The value is the proxy's own address, and for most proxies it starts with http:// even when the website is HTTPS:
import requests
proxy = "http://127.0.0.1:8901"
proxies = {"http": proxy, "https": proxy}
response = requests.get("http://127.0.0.1:8000/ip", proxies=proxies, timeout=10)
print(response.status_code, response.json())
200 {'path': '/ip', 'via': 'proxy-a-8901', 'authorization_sent_to_site': False}
For an HTTPS website, Requests asks the proxy to open a tunnel (CONNECT) and then speaks TLS to the website through it. The proxy URL stays http://:
from urllib.parse import quote
import requests
proxy = f"http://lab:{quote('p@ss#w0rd', safe='')}@127.0.0.1:8899"
# The "https" key is the scheme of the website. The proxy itself is still http://
response = requests.get("https://example.com/", proxies={"https": proxy}, timeout=15)
print(response.status_code, response.text.split("<title>")[1].split("</title>")[0])
200 Example Domain
If you set only the http key, HTTPS URLs bypass the proxy and go out from your own IP. Set both unless you mean it.
Proxy authentication
Put the username and password in the proxy URL: http://USER:PASSWORD@HOST:PORT. Requests turns them into a Proxy-Authorization header for the proxy. The website never sees them (authorization_sent_to_site: False above).
Do not use auth= for the proxy
auth= sets the Authorization header for the website. The proxy never gets credentials and refuses the request:
import requests
proxies = {"http": "http://127.0.0.1:8899", "https": "http://127.0.0.1:8899"}
# auth= is sent to the website, not to the proxy
response = requests.get("http://127.0.0.1:8000/ip", proxies=proxies, auth=("lab", "p@ss#w0rd"), timeout=10)
print(response.status_code, response.reason)
try:
requests.get("https://example.com/", proxies=proxies, auth=("lab", "p@ss#w0rd"), timeout=10)
except requests.exceptions.ProxyError as error:
print("ProxyError:", "Tunnel connection failed" in str(error), "407" in str(error))
407 Proxy Authentication Required
ProxyError: True True
For an HTTP website you get the proxy's 407 back as a normal response. For an HTTPS website the tunnel fails and Requests raises ProxyError with Tunnel connection failed: 407 Proxy Authentication Required.
Percent-encode special characters
Some characters in a username or password break the proxy URL. A #, for example, starts a URL fragment. Encode both parts with urllib.parse.quote(value, safe='') and you never have to check which characters are safe:
import requests
for password in ["p@ss#w0rd", "p%40ss%23w0rd"]:
proxy = f"http://lab:{password}@127.0.0.1:8899"
try:
response = requests.get("http://127.0.0.1:8000/ip", proxies={"http": proxy}, timeout=10)
print(password, "->", response.status_code, response.reason)
except Exception as error:
print(password, "->", type(error).__name__, str(error)[:80])
p@ss#w0rd -> ProxyError HTTPConnectionPool(host='ss', port=80): Max retries exceeded with url: http://12
p%40ss%23w0rd -> 200 OK
The unencoded # starts a URL fragment, so Requests tried to connect to a proxy called ss on port 80. The encoded password works.
Proxies from environment variables
Requests reads HTTP_PROXY, HTTPS_PROXY, ALL_PROXY and NO_PROXY (upper or lower case) when you don't pass proxies. This is handy for a whole script, and surprising when a shell variable you forgot about takes effect:
import os
import requests
os.environ["HTTP_PROXY"] = "http://127.0.0.1:8901"
os.environ["NO_PROXY"] = "localhost"
print(requests.get("http://127.0.0.1:8000/env").json()["via"]) # picked up from HTTP_PROXY
print(requests.get("http://localhost:8000/env").json()["via"]) # NO_PROXY match, goes direct
session = requests.Session()
session.proxies = {"http": "http://127.0.0.1:8902"}
print(session.get("http://127.0.0.1:8000/env").json()["via"]) # environment wins over session.proxies
session.trust_env = False
print(session.get("http://127.0.0.1:8000/env").json()["via"]) # session.proxies used
print(session.get("http://127.0.0.1:8000/env", proxies={"http": "http://127.0.0.1:8901"}).json()["via"]) # per-request wins
proxy-a-8901
None
proxy-a-8901
proxy-b-8902
proxy-a-8901
The third line is the trap: with an environment proxy set, session.proxies was ignored. Set session.trust_env = False when a session must use its own proxy, or pass proxies= on each request, which always wins.
SOCKS5 proxies
Install the SOCKS extra first: pip install "requests[socks]" (it adds PySocks). Then use a socks5:// or socks5h:// URL:
import requests # pip install "requests[socks]"
proxy = "socks5h://lab:secret@127.0.0.1:1080" # socks5h: the proxy resolves hostnames
response = requests.get("http://localhost:8000/socks", proxies={"http": proxy, "https": proxy}, timeout=10)
print(response.status_code, response.json())
200 {'path': '/socks', 'via': None, 'authorization_sent_to_site': False}
With socks5:// your machine resolves the hostname and sends the IP to the proxy. With socks5h:// the proxy resolves it, which is what you want when the DNS lookup should not come from your network. (via is empty here because only the HTTP proxies in the lab stamp that header.)
Rotating proxies
To spread requests over a pool, cycle through the list and move on when a proxy fails. This version skips dead proxies instead of crashing:
import itertools
import requests
POOL = ["http://127.0.0.1:8901", "http://127.0.0.1:8903", "http://127.0.0.1:8902"] # 8903 is down
rotation = itertools.cycle(POOL)
def get(url, attempts=3):
for _ in range(attempts):
proxy = next(rotation)
try:
response = requests.get(url, proxies={"http": proxy, "https": proxy}, timeout=5)
response.raise_for_status()
return proxy, response
except (requests.exceptions.ProxyError, requests.exceptions.ConnectTimeout) as error:
print("skip", proxy, type(error).__name__)
raise RuntimeError("all attempts failed")
for page in range(1, 5):
proxy, response = get(f"http://127.0.0.1:8000/page/{page}")
print(page, proxy, "->", response.json()["via"])
1 http://127.0.0.1:8901 -> proxy-a-8901
skip http://127.0.0.1:8903 ProxyError
2 http://127.0.0.1:8902 -> proxy-b-8902
3 http://127.0.0.1:8901 -> proxy-a-8901
skip http://127.0.0.1:8903 ProxyError
4 http://127.0.0.1:8902 -> proxy-b-8902
In production you would also drop a proxy that keeps failing and slow down when the site returns 429. If you would rather not run the pool at all, the proxy mode card above rotates exit IPs behind one proxy URL, and ScrapingAnt residential proxies give residential IPs with country targeting for your own scraper.
Proxy errors and what they mean
| What you see | Usual cause |
|---|---|
407 Proxy Authentication Required response | Credentials missing or passed with auth= instead of in the proxy URL |
ProxyError: ... Tunnel connection failed: 407 | Same, for an https:// website |
ProxyError: HTTPConnectionPool(host='ss', port=80) or another odd host | Unencoded special characters (such as #) in the credentials |
ProxyError: ... Unable to connect to proxy | Wrong host or port, or the proxy is down |
ConnectTimeout | Proxy not reachable in time; set timeout= so this fails fast |
InvalidSchema: Missing dependencies for SOCKS support | requests[socks] is not installed |
SSLError through a proxy | The proxy re-signs HTTPS traffic. For your own intercepting proxy, trust its CA with verify="ca.pem"; for a provider that documents verify=False (like ScrapingAnt proxy mode), set it on those requests only |
Free or paid proxies
Free proxies are fine for a test and poor for anything else: they are slow, disappear without notice and are often already blocked by popular sites. We publish a list of free proxies for testing, and the problems with free proxies explains the risks. For choosing between datacenter, residential and mobile IPs, see main proxy types, and to run your own forward proxy in Python, see proxy.py.
FAQ
How do I check that Requests is really using the proxy?
Request a page that echoes your IP, such as https://ipinfo.io/json, with and without proxies. The IP should change. Also check that no HTTP_PROXY or HTTPS_PROXY variable is overriding your settings (see above).
Why does my HTTPS request ignore the proxy?
Your proxies dict probably has only an http key. Add "https": "http://HOST:PORT".
Can I use a different proxy for each request in a Session?
Yes. Pass proxies= to session.get(); a per-request value wins over session.proxies and over environment variables.
Should I write https:// in the proxy URL?
Only if your provider gives you an HTTPS proxy endpoint, which encrypts the connection to the proxy itself. Most providers give an http:// endpoint, and HTTPS websites still work through it via CONNECT.
Examples tested on 2026-10-26 with Python 3.12.11, requests 2.34.2, urllib3 2.8.0 and PySocks 1.7.1. Code: scrapingant-examples/examples/python-requests-proxy.
This article was drafted with AI assistance from a tested evidence packet and reviewed by the named author, who is responsible for the code, measurements and corrections.