NEWScrapingAnt MCP for Claude Code, Cursor & Windsurf — try it free →
Skip to main content

Python Requests Proxy: Setup, Authentication, SOCKS5 and Rotation

· 10 min read
Oleg Kulyk
Co-Founder @ ScrapingAnt

Python Requests Proxy | How to Use Proxy Types in Python Requests

Correction (2026-10-26)

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 seeUsual cause
407 Proxy Authentication Required responseCredentials missing or passed with auth= instead of in the proxy URL
ProxyError: ... Tunnel connection failed: 407Same, for an https:// website
ProxyError: HTTPConnectionPool(host='ss', port=80) or another odd hostUnencoded special characters (such as #) in the credentials
ProxyError: ... Unable to connect to proxyWrong host or port, or the proxy is down
ConnectTimeoutProxy not reachable in time; set timeout= so this fails fast
InvalidSchema: Missing dependencies for SOCKS supportrequests[socks] is not installed
SSLError through a proxyThe 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.

Proxies for scraping, in the country you need

Use ScrapingAnt datacenter and residential proxies. Picking a country costs nothing extra: every proxy country works on both pools at the normal price.