Skip to content

How to use proxies with Python requests

Set a proxy in Python requests and fix what breaks: the https key, trust_env, socks5h and the error each failure raises. Tested with requests 2.34.2.

HProxy TeamJuly 3, 2026Updated September 15, 202614 min read

How to Use Proxies With Python Requests

To send Python requests through a proxy, pass a dictionary to the proxies argument. Give it an http key and an https key, both set to the proxy URL, and set a timeout. The keys name the scheme of the page you request, not the protocol of the proxy. If the https key is missing, https:// pages skip the proxy.

import requests

proxies = {
    "http": "http://203.0.113.7:8080",
    "https": "http://203.0.113.7:8080",
}
r = requests.get("https://example.com", proxies=proxies, timeout=(6.05, 30))

We tested each behaviour on this page on 15 September 2026. We used requests 2.34.2, urllib3 2.7.0 and PySocks 1.7.1 on Python 3.13, against test proxies on our own machine. So the results show what requests does, not what one provider does. Where a result differs from the guides that rank for this topic, the section says so.

Summary of our test run with requests 2.34.2: with only the http key the proxy saw 0 CONNECT requests; HTTP_PROXY beat session.proxies; a proxy that never answered raised ProxyError; a 407 on an https URL raised ProxyError; socks5 looked up example.com on our machine and socks5h did not; ten requests.get calls opened 10 connections and one Session opened 1.
Output of our test run, 15 September 2026. Every line is read from the results of that run.

Before you start

  • Python 3.10 or later. PyPI lists that as the minimum for requests 2.34.2, released on 14 May 2026.
  • requests itself: python -m pip install requests. Check your version with python -m pip show requests, and use 2.31.0 or later (see step 2).
  • For SOCKS proxies: python -m pip install "requests[socks]".
  • A proxy address: host, port, and a username and password if the proxy asks for them. For a test, our free proxy API returns addresses from our checked free pool as plain text, with no key.

Step 1: set the proxies dictionary

The dictionary maps a URL scheme to a proxy URL. requests reads the scheme of the page you request and picks the matching key. So the https key means: send my https:// requests through this proxy. The scheme inside the value, http:// here, says how requests talks to the proxy itself.

An HTTP proxy carries https:// traffic through a tunnel. requests sends the proxy a CONNECT request, and TLS then runs inside the tunnel to the site. RFC 9110 defines that method in section 9.3.6. That is why "https": "http://..." is the normal pair.

With only an http key, https:// requests do not use the proxy. In our test, the proxy saw no CONNECT at all, and the request reached the site directly. The site then sees your own IP address, and no error tells you.

With only the http key, https traffic skips the proxy
  1. https:// request

    no https key

  2. Key lookup

    nothing matches https

  3. Direct connection

    the proxy saw 0 CONNECT

  4. Site

    sees your own IP

Source: Our test with requests 2.34.2, 15 September 2026 · hproxy.comProxy.

Do not give the https key an https:// proxy URL unless your provider says the proxy takes TLS connections. Several of the guides that rank for this topic show that pair. Against a plain HTTP proxy, it fails before any request leaves:

requests.exceptions.ProxyError: ... (Caused by ProxyError('Unable to connect to proxy. Your proxy appears to only use HTTP and not HTTPS, try changing your proxy URL to be HTTP. ...'))

Two more keys help. A key with a scheme and a host, such as https://api.example.com, wins over the plain https key for that host. An all key covers every scheme that has no key of its own. requests checks the keys in this order: scheme and host, scheme, all with host, then all.

proxies = {
    "https": "http://203.0.113.7:8080",                      # every https:// page
    "https://api.example.com": "http://198.51.100.14:3128",  # this host only
}

Always write the scheme in the proxy URL. A bare 203.0.113.7:8080 worked in our test, but the requests documentation says proxy URLs must include the scheme.

Step 2: add the username and password

Put the username and password in the proxy URL, as user:password@host:port. requests sends them to the proxy in a Basic Proxy-Authorization header.

proxy = "http://myuser:mypassword@203.0.113.7:8080"
proxies = {"http": proxy, "https": proxy}
r = requests.get("https://example.com", proxies=proxies, timeout=(6.05, 30))

Encode the password if it contains @, : or /. Without that, requests 2.34.2 raised InvalidURL: Failed to parse in our test. The message printed the whole proxy URL, password included, so keep such errors out of shared logs. Use quote() with safe="", because by default it leaves / as it is:

from urllib.parse import quote

user = "myuser"
password = quote("p@ss:word/!", safe="")   # p%40ss%3Aword%2F%21
proxy = f"http://{user}:{password}@203.0.113.7:8080"
proxies = {"http": proxy, "https": proxy}

requests decodes the password before it sends it. In our test, the proxy received p@ss:word/! exactly.

A wrong or missing password shows up in two ways. RFC 9110 defines status 407 for a proxy that needs a login, sent with a Proxy-Authenticate challenge. On an http:// page, requests returns that 407 as a normal response, with no exception. On an https:// page, the 407 answers the CONNECT, and requests raises this instead:

requests.exceptions.ProxyError: ... (Caused by ProxyError('Unable to connect to proxy', OSError('Tunnel connection failed: 407 Proxy Authentication Required')))

Check your version too. requests 2.3.0 to 2.30.0 could leak the Proxy-Authorization header to the site on redirects to HTTPS, when the password sat in the proxy URL. The advisory for CVE-2023-32681, published on 22 May 2023, names 2.31.0 as the fix. In our test with 2.34.2, a site that redirected between two https:// pages never received the header.

The requests documentation also warns against keeping proxy passwords in a variable of the shell or in a file under version control.

Step 3: reuse one Session

requests.get() creates a new Session for each call, so each call opens a new connection. A requests.Session keeps its connections and reuses them. In our test, ten requests.get() calls through one proxy opened 10 connections and 10 CONNECT tunnels. Ten requests on one Session opened 1 connection and 1 tunnel.

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

r1 = s.get("https://example.com/one", timeout=(6.05, 30))
r2 = s.get("https://example.com/two", timeout=(6.05, 30))   # same tunnel

There is one trap. If HTTP_PROXY or HTTPS_PROXY is set, requests uses it instead of session.proxies. In our test, a Session sent its request through the HTTP_PROXY proxy, although session.proxies named another one. The requests documentation warns about this and links issue #2018. That issue was opened on 25 April 2014 and was still open on 15 September 2026.

Two fixes worked in our test. Pass proxies= on each request, which beat the variable. Or set trust_env = False on the Session:

s = requests.Session()
s.trust_env = False          # ignore proxy variables and system proxy settings
s.proxies.update(proxies)
r = s.get("https://example.com", timeout=(6.05, 30))

Why does requests use a proxy you did not set?

When a request carries no proxies=, requests reads http_proxy, https_proxy, all_proxy and no_proxy, in lowercase or uppercase. When none is set, Python looks for the system proxy settings: the registry on Windows and System Configuration on macOS. So a proxy set for the whole system can carry your script's traffic too.

Session.trust_env switches all of this off. In the requests 2.34.2 code, trust_env = False skips the proxy variables and system settings. It also skips passwords from a .netrc file and the REQUESTS_CA_BUNDLE and CURL_CA_BUNDLE variables. Our test confirmed the proxy, .netrc and CA bundle cases.

This is what we measured with requests 2.34.2:

Setting in our testProxy that handled the request
HTTP_PROXY set, no proxies= in the codeThe HTTP_PROXY proxy
HTTP_PROXY set, session.proxies names another proxyThe HTTP_PROXY proxy
The same, with session.trust_env = FalseThe session.proxies proxy
HTTP_PROXY set, proxies= passed on the requestThe proxy passed on the request
HTTP_PROXY set, NO_PROXY=localhost, request to localhostNone, the request went direct
HTTP_PROXY set, NO_PROXY=*None, the request went direct
HTTP_PROXY set, proxies={"http": None}None, the request went direct
ALL_PROXY set, proxies={"http": None, "https": None}The ALL_PROXY proxy
proxies holds an http proxy and "no_proxy": "localhost"The http proxy, for localhost too

Two rows are easy to get wrong. Setting both keys to None does not switch off ALL_PROXY, because requests falls back to the all key. And a no_proxy key inside the dictionary only filters proxies taken from the environment, not the proxies in the same dictionary.

If you rely on NO_PROXY rules, test them after an upgrade. The notes for requests 2.34.0, released on 11 May 2026, say it no longer performs greedy matching on no_proxy domains.

Step 4: use a SOCKS5 proxy

requests speaks to HTTP proxies by itself. SOCKS needs the socks extra, which installs PySocks:

python -m pip install "requests[socks]"

Without it, a SOCKS proxy URL raises InvalidSchema: Missing dependencies for SOCKS support. Once it is installed, use a socks5h:// URL:

proxy = "socks5h://myuser:mypassword@203.0.113.7:1080"
proxies = {"http": proxy, "https": proxy}
r = requests.get("https://example.com", proxies=proxies, timeout=(6.05, 30))

The h decides who looks up the host name. The requests documentation says socks5 resolves names on the client and socks5h on the proxy. We checked what reaches the proxy. With socks5://, our machine looked up example.com, and the proxy received an IPv6 address. With socks5h://, our machine made no lookup, and the proxy received the name example.com. SOCKS5 allows either form, as RFC 1928 defines.

So use socks5h:// when the lookups should not come from your own network, or when a name only resolves on the proxy's side. The username and password from the URL reached our test proxy through SOCKS5 login, the method RFC 1929 defines.

A SOCKS proxy that refused the connection raised requests.exceptions.ConnectionError in our test, not ProxyError. Catch both if you mix proxy types. Our guide to SOCKS5 proxies covers the protocol itself.

Step 5: set a timeout

requests does not time out unless you set a timeout. Its documentation warns that code without one may hang for minutes or more. Pass two numbers: the first limits the connection, the second limits the wait for data. The documentation suggests a connect value a little above a multiple of 3, the default TCP retry window.

r = requests.get("https://example.com", proxies=proxies, timeout=(6.05, 30))

The second number is not a limit on the whole download. The documentation says it is the wait between bytes from the server. A download that keeps sending data can run longer than the number you set.

Through a proxy, the two numbers apply in ways that are easy to misread. With timeout=(3, 4) on an http:// page, a proxy that took the connection and went silent raised ReadTimeout after 4 seconds. On an https:// page, a proxy that never answered the CONNECT, or went quiet after it, raised ReadTimeout after 3 seconds. The message reported a read timeout of 3, the connect value.

Step 6: catch the error the failure really raises

The exception names do not always match the problem. We caused each failure on purpose against a test proxy. This is what requests 2.34.2 raised:

What went wrongWhat requests raisedWhat to check
Nothing listens on the proxy portProxyError: Unable to connect to proxyHost and port
The proxy address never answersProxyError after the connect timeout, not ConnectTimeoutThe address, and whether the proxy is up
Wrong or missing password, https:// pageProxyError: Tunnel connection failed: 407Username, password, encoding
Wrong or missing password, http:// pageNo exception; r.status_code is 407The same, then the status code
https:// proxy URL for an HTTP proxyProxyError: proxy appears to only use HTTPChange the value to http://
The proxy takes the connection, then stays silentReadTimeoutThe proxy, or another address
The site is slow behind a working proxyReadTimeout after the read timeoutThe site, or a longer read timeout
A SOCKS proxy refuses the connectionConnectionErrorThe SOCKS address and its rules
A SOCKS URL without PySocksInvalidSchema: Missing dependencies for SOCKS supportpip install "requests[socks]"
A password with @, : or /, not encodedInvalidURL: Failed to parsequote(password, safe="")

In requests, ProxyError is a kind of ConnectionError, and ConnectTimeout is both a ConnectionError and a Timeout. All of them come from RequestException. Order the except blocks from the most specific to the most general:

from requests.exceptions import ConnectionError, ProxyError, ReadTimeout, RequestException

try:
    r = requests.get(url, proxies=proxies, timeout=(6.05, 30))
    r.raise_for_status()
except ProxyError as e:
    print(f"proxy failed: {e}")         # unreachable, silent, or asking for a login
except ReadTimeout:
    print("no data in time")            # connected, then nothing arrived
except ConnectionError as e:
    print(f"connection failed: {e}")    # SOCKS refusals and other connection errors
except RequestException as e:
    print(f"request failed: {e}")

An except ConnectTimeout block does not catch a proxy that never answers. Two of the guides we read use one for proxy timeouts, and in our test that failure raised ProxyError.

Do not switch off certificate checks to make an error go away. The requests documentation says verify=False leaves an application open to man-in-the-middle attacks, and should only be set for testing.

Step 7: retry with backoff

requests does not retry failed connections by default. A urllib3 Retry mounted on the Session adds retries with a growing pause:

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

s = requests.Session()
retry = Retry(
    total=3,
    backoff_factor=0.5,                       # 0 s, 1 s, 2 s between tries
    status_forcelist=[429, 500, 502, 503, 504],
    allowed_methods={"GET", "HEAD"},
)
adapter = HTTPAdapter(max_retries=retry)
s.mount("http://", adapter)
s.mount("https://", adapter)

We ran this against a proxy that answered 503 every time. requests made 4 attempts, 0, 1 and 2 seconds apart, then raised RetryError. In the urllib3 code, the first retry comes at once, and each later pause doubles, up to 120 seconds by default.

All 4 attempts went to the same proxy, because the retries run inside one request. So use Retry for short failures on a stable proxy. To try a different address, rotate in your own code.

Step 8: rotate across a pool

To spread requests over several addresses, pick one per attempt and move on when it fails:

import random
import requests

POOL = [
    "203.0.113.7:8080",
    "203.0.113.24:3128",
    "198.51.100.14:8080",
    "198.51.100.66:8000",
]

def get_rotating(url, tries=4):
    for _ in range(tries):
        address = random.choice(POOL)
        proxies = {"http": f"http://{address}", "https": f"http://{address}"}
        try:
            r = requests.get(url, proxies=proxies, timeout=(6.05, 30))
            r.raise_for_status()
            return r
        except requests.RequestException:
            continue   # dead or blocked address, try another
    raise RuntimeError(f"all {tries} tries failed for {url}")

html = get_rotating("https://example.com").text

You can fill the list from our free proxy API. It returns plain text by default, and protocol and limit narrow the list. Add recent=true to include addresses that worked in the last 48 hours as well.

POOL = requests.get(
    "https://hproxy.com/api/proxy-list",
    params={"format": "txt", "protocol": "http", "limit": 50},
    timeout=15,
).text.split()

Step 9: check that the proxy works

Check a proxy before you trust it. Compare the address a server sees with and without the proxy. Our echo endpoint returns the address it sees and the request headers as JSON:

import requests

ECHO = "https://hproxy.com/api/free-proxy/echo"
proxies = {"http": "http://203.0.113.7:8080", "https": "http://203.0.113.7:8080"}

direct = requests.get(ECHO, timeout=10).json()
via_proxy = requests.get(ECHO, proxies=proxies, timeout=(6.05, 30)).json()

print("without proxy:", direct["ip"])
print("with proxy:   ", via_proxy["ip"])
assert direct["ip"] != via_proxy["ip"], "the proxy is not changing your address"

# Your own address in any header means the proxy passes it on.
leaks = [name for name, value in via_proxy["headers"].items() if direct["ip"] in value]
print("headers with your address:", leaks or "none")

If both addresses match, the proxy is not carrying your traffic. Check the https key from step 1 first.

Our proxy checker shows the same in a browser. For each proxy, it lists the status, protocol, anonymity, location, network and latency.

Where free proxies stop working

Free proxies are fine for testing the code on this page. For a script that runs unattended, the numbers from our own free pool show the limit. On 5 September 2026, the pool held 9,451 live proxies out of 72,230 in rotation. The median live proxy passed 45.5 percent of its own checks. Of the 691,179 proxies we watched die, the median one lasted 144.5 hours, and 22.6 percent died within the first hour.

A paid pool removes that list work. With our residential proxies, a username without a session ID rotates the exit address, and a username with one keeps it. They are billed per gigabyte.

Where to go from here

Our cURL guide shows the same settings in a shell, and proxies for web scraping covers the choice of proxy type. How to avoid IP bans while scraping is the checklist for when rotation is not enough. For a larger crawler, proxies with Scrapy moves rotation and retries into a framework. To manage proxies from code, see our API documentation.

What this page could not check

We ran every test on Windows 11, against test proxies on our own machine, not against a provider's gateway. The exception classes come from requests and urllib3 code that is the same on every system. The text inside them differs, such as Windows error numbers. We did not test proxies that take TLS connections, Digest proxy logins, or async clients such as httpx and aiohttp. The HTTP_PROXY rule over session.proxies is tracked in an open issue, so a later release may change it. We will check these results again by 15 March 2027.

Sources

Frequently asked questions

Why is my Python requests proxy being ignored?

Three settings cause it. The dictionary has only an http key, so https:// requests connect directly. Or HTTP_PROXY or HTTPS_PROXY is set, and in our test with requests 2.34.2 the variable beat session.proxies. Or the host matches NO_PROXY. Set both keys, pass proxies= on each request, or set trust_env = False on the Session.

How do I make requests ignore HTTP_PROXY?

Set trust_env = False on a Session. The Session then stops reading proxy variables and the system proxy settings. For one request, proxies={'http': None, 'https': None} skips HTTP_PROXY and HTTPS_PROXY, but not ALL_PROXY: in our test that request still used the ALL_PROXY proxy. NO_PROXY=* sends every request direct.

What does session.trust_env = False turn off?

Everything requests reads from outside your code: the proxy variables, NO_PROXY, the system proxy settings Python finds on Windows and macOS, passwords in a .netrc file, and the REQUESTS_CA_BUNDLE and CURL_CA_BUNDLE variables. In our test, a Session with trust_env = False ignored HTTP_PROXY, a .netrc file and REQUESTS_CA_BUNDLE.

What is the difference between socks5:// and socks5h:// in requests?

With socks5://, your machine looks up the host name and sends the proxy an IP address. With socks5h://, the proxy does the lookup. In our test, socks5:// looked up example.com on our machine, and socks5h:// sent the proxy the name instead. Both need the socks extra: python -m pip install requests[socks].

What does Tunnel connection failed: 407 Proxy Authentication Required mean?

The proxy wants a username and password, or rejected the ones you sent. On an https:// URL, requests raises ProxyError with that text. On an http:// URL, it returns a normal response with status code 407 instead. Check the username and password, and percent-encode any @, : or / in the password.

Should the https key use an https:// proxy URL?

Only if your provider says the proxy takes TLS connections. The key names the scheme of the page you request. The value says how requests reaches the proxy. For a plain HTTP proxy, an https:// value fails: in our test, requests raised ProxyError saying the proxy appears to only use HTTP and not HTTPS.

Does a proxy timeout raise ConnectTimeout?

Not in our tests with requests 2.34.2. A proxy address that never answered raised ProxyError after the connect timeout. A proxy that took the connection and then went silent raised ReadTimeout. For proxy problems, catch ProxyError and ReadTimeout, and keep RequestException as the last catch.

Get proxies that are alive right now

Our free proxy list re-checks every exit every few minutes across 100+ countries, with a live last-checked time, so you copy IPs that worked moments ago, not a stale text dump. When the location has to survive a real check, the paid network holds up.

129M+ proxy checks run · 100+ countries · HTTP / HTTPS / SOCKS · re-checked every few minutes · no signup

HProxy.

Honest guides and comparisons on proxies, scraping and staying unblocked, from the team that runs the network.

RSS feed