How to Use a Proxy with Python Requests: Setup, Authentication & Rotation 2026
Using Python to route your HTTP requests through a proxy can be a real revolution when you need your traffic to go through a middleman server. Setting up a proxy server with the Python Requests library is simple—just instruct it to route your requests through a proxy server. Whether you’re web scraping, trying to access stuff that’s blocked in your area, or just experimenting, knowing how to work with Python requests proxy server is super handy. Proxying your requests in Python helps keep you under the radar, prevents sites from blocking you, and even allows you to rotate your IPs for more privacy. In this Python scraping guide, we’ll go over everything—from jumping into a basic proxy set-up to more advanced stuff like proxy-auth set-up, session-set management, using environment variables, rotating proxy servers for scraping, picking the best proxy servers, and troubleshooting common issues.
TL;DR: Set a Proxy in Python Requests in 30 Seconds
If you came here for the quick fix, here it is. Pass a proxies dict to your request, with both http and https keys pointing to your proxy:
```python
import requests
proxies = {
"http": "http://USER:PASS@proxy.example.com:8080",
"https": "http://USER:PASS@proxy.example.com:8080",
}
resp = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10)
print(resp.json()) # should show the proxy's IP, not yours
```
That’s for the basic case. The rest of this guide covers what happens when this doesn’t “just work”: authentication errors, SOCKS5 proxies, rotation for scraping, environment variables, and the weird edge cases that eat up afternoons.
Quick Start: Setting Up a Proxy in Python Requests

Let’s get right into sending HTTP requests through a proxy in Python. First, you’ll want to set up the requests library, and then you can set it to make requests over a proxy server. It’ll also be a good chance to see how to set a username and password for proxy authentication in your Python code, too.
Installing the Requests Library
Install the requests library using pip if you haven’t already:
pip install requests
After installing, import it into your Python program (import requests) to use.
Basic Proxy Configuration in Python Requests
The Python requests library enables proxying HTTP requests in Python through a proxy dictionary definition. The dictionary is set up by protocols and backed by the proxy URL. Assuming your proxy server is at 123.45.67.89:8080, set it up like this in Python: Here we’ve passed a proxy dictionary to requests.get. This proxy server, once set, will capture your Python request and send it to the target site. The response to the target will be received by a different IP than yours, confirming that your Python request came via the proxy server:
python
import requests
# Define proxy server address for HTTP and HTTPS
proxies = {
'http': 'http://<YOUR_PROXY_HERE>:8080',
'https': 'http://<YOUR_PROXY_HERE>:8080'
}
response = requests.get('http://httpbin.org/ip', proxies=proxies)
print(response.text)
Note: In Python, always set both ‘http’ in the proxy’s dictionary. If you fail to set one and make a request to that kind of URL, that request won’t use the proxy.
How to verify your proxy is actually working
Sending a request through a proxy and getting a 200 back doesn’t mean the proxy was used, Requests can silently fall back to a direct connection in some failure modes. The only reliable check is to ask a service that echoes your IP and compare what comes back to your real public IP.
```python
import requests
proxies = {
"http": "http://USER:PASS@proxy.example.com:8080",
"https": "http://USER:PASS@proxy.example.com:8080",
}
```
1. Get your real IP (no proxy)
real_ip = requests.get("https://api.ipify.org").text
2. Get your IP through the proxy
proxy_ip = requests.get("https://api.ipify.org", proxies=proxies, timeout=10).text
print(f"Real IP: {real_ip}")
print(f"Proxy IP: {proxy_ip}")
assert real_ip != proxy_ip, "Proxy didn't change your IP — check your config"
```
Use https://api.ipify.org, https://httpbin.org/ip, or https://ifconfig.me/ip: they all just return the IP that made the request, in plain text or JSON. If the two values match, your proxy isn’t being applied: most often the https key is missing from your dict, the proxy is down, or trust_env is overriding you from an environment variable.
Python Requests Proxy Authentication: Username, Password & Common Errors
Most paid proxies don’t let just anyone in, you’ll need to authenticate. Python Requests handles this through the proxy URL itself: you embed credentials directly into the string, and the library takes care of the Proxy-Authorization header for you.
Basic auth in the proxy URL
The format is http://username:password@host:port. Same dict shape as before, just with credentials baked in:
```python
import requests
proxies = {
"http": "http://myuser:mypassword@proxy.example.com:8080",
"https": "http://myuser:mypassword@proxy.example.com:8080",
}
resp = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10)
print(resp.status_code)
```
If credentials are correct, the request goes through. If not, you’ll get a 407 Proxy Authentication Required — that’s the proxy server telling you the username or password is wrong.
URL-encoding special characters in the password
Here’s the gotcha that eats hours: if your password contains @, :, /, %, or #, the URL parser will break. The fix is to URL-encode the password before slotting it into the proxy string:
```python
import requests
from urllib.parse import quote
username = "myuser"
password = "p@ss:word#1" # has special characters
encoded = quote(password, safe="") # → 'p%40ss%3Aword%231'
proxies = {
"http": f"http://{username}:{encoded}@proxy.example.com:8080",
"https": f"http://{username}:{encoded}@proxy.example.com:8080",
}
resp = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10)
print(resp.json())
```
This is the #1 cause of mysterious 407 errors when the password “looks right” but the request still fails. Always quote.
IP whitelisting as an alternative
Some providers (ProxyWing included) let you skip the username/password dance entirely by whitelisting your server’s outgoing IP. You authorize the IP once in the dashboard, and after that any request from that machine works without credentials in the URL:
```python
proxies = {
"http": "http://proxy.example.com:8080",
"https": "http://proxy.example.com:8080",
}
resp = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10)
```
This is cleaner for production: no secrets in environment variables, no encoding headaches. The trade-off is that you’re locked to a static IP — fine for a server, painful for a laptop on Wi-Fi.
Authentication errors and how to fix them
| Error | What it means | Fix |
| 407 Proxy Authentication Required | Wrong credentials or unencoded special chars | Verify user/pass, URL-encode the password |
| 403 Forbidden | Proxy works, but target site blocked the request | Rotate IPs, lower request rate |
| 502 Bad Gateway | Proxy is up but can’t reach the target | Try a different proxy in the pool |
| Connection refused | Proxy is down or wrong host/port | Check the endpoint, try another proxy |
| SSL handshake failed | Proxy intercepts HTTPS without a trusted cert | Use verify=False for debug only, install proxy’s CA cert for production |
Using Python Requests with Different Proxy Setups

With the basics out of the way, let’s discuss some special cases. Let’s discuss the use of a persistent session with request-bound proxy connections, specifying proxies in environment variables, and a SOCKS proxy server.
Setting Up a Proxy Session for Persistent Connections
If you are going to make many requests to the same proxy, use a request. Session() to avoid having to specify the proxy configuration each time. A session also reuses a TCP connection, which will help with performance.
python
import requests
session = requests.Session()
session.proxies = {
'http': 'http://<YOUR_PROXY_HERE>:8080',
'https': 'http://<YOUR_PROXY_HERE>:8080'
}
response = session.get('http://httpbin.org/ip')
print(response.text)
In this example, we set up the proxies only once in a session. Any request made with session.get() or session.post() will default to the configured route. A session can be helpful in the case of web scraping when you want each request to make a consistent path over the same routing setup without repeatedly defining it.
Using Environment Variables for Proxy Configuration
Python Requests parses proxy settings from the environment variables, so don’t hardcode them. Configure HTTP_PROXY in your environment (export under Linux/MacOS, set in Windows):
bash
export HTTP_PROXY="http://<YOUR_PROXY_HERE>:8080"
export HTTPS_PROXY="http://<YOUR_PROXY_HERE>:8080"
If you have set these within your terminal, any Python program that utilizes the requests library will be proxied by default. For instance, typing:
python
import requests
response = requests.get('http://httpbin.org/ip')
print(response.text)
will use the proxy server in the environmental variables, even though we have not passed a proxy’s dictionary to code. This is useful to enable or disable the proxies for specific request scenarios without needs to change the script.
Disabling environment proxies on a per-session basis
By default, requests.Session() reads HTTP_PROXY / HTTPS_PROXY from your environment — that’s trust_env=True. If you want one specific session to ignore those env variables (for example, when you’ve set proxies for the whole system but want one script to go direct), set trust_env to False:
```python
import requests
s = requests.Session()
s.trust_env = False # ignore HTTP_PROXY / HTTPS_PROXY
resp = s.get("https://httpbin.org/ip")
```
Bypassing the proxy for specific hosts with NO_PROXY
If you want most traffic to go through a proxy but a few internal hosts to go direct, use the NO_PROXY environment variable. It takes a comma-separated list of hostnames or domains:
```bash
export HTTP_PROXY="http://proxy.example.com:8080"
export HTTPS_PROXY="http://proxy.example.com:8080"
export NO_PROXY="localhost,127.0.0.1,.internal.company.com"
```
Requests will route external traffic through the proxy but call localhost, 127.0.0.1, and anything under .internal.company.com directly. You can also override this per-request with proxies={"no_proxy": "localhost"}, but the env var is cleaner for most setups.
Handling HTTP, HTTPS, and SOCKS Proxies
By default, the requests library supports HTTPS proxies. For a SOCKS proxy (e.g., socks5:// for Tor or similar), you need to install support via:
bash
pip install requests[socks]
Then you can use a SOCKS proxy by specifying the scheme in the URL:
python
import requests
proxies = {
'http': 'socks5://<YOUR_PROXY_HERE>:1080',
'https': 'socks5://<YOUR_PROXY_HERE>:1080'
}
response = requests.get('http://httpbin.org/ip', proxies=proxies)
print(response.text)
This will proxy the request via a SOCKS5 proxy. You can supply a username and a password if your SOCKS proxy requires them by adding them in the URL like this. HTTP proxies are directly supported in requests, and SOCKS proxies are supported too if you include additional dependency. You’ve set up basic proxies, sessions, env-vars, and SOCKS proxies in Python. Next comes a deeper dive into rotating proxies for web scraping.
socks5:// vs socks5h:// — why DNS matters
There’s a subtle but important difference between the two SOCKS5 schemes:
socks5://— your client resolves the hostname locally, then sends the IP to the proxy. Your DNS query leaks.socks5h://— your client sends the hostname to the proxy, and the proxy resolves it. No DNS leak.
For anonymity or geo-targeting (where you want DNS resolution from the proxy’s location, not yours), always use socks5h://:
```python
import requests
proxies = {
"http": "socks5h://user:pass@proxy.example.com:1080",
"https": "socks5h://user:pass@proxy.example.com:1080",
}
resp = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10)
print(resp.json())
```
If you forget the h, the request will still work, but anyone watching your DNS can see what sites you’re visiting, defeating most of the point of using a SOCKS proxy in the first place.
Rotating Proxies in Python Requests for Web Scraping

When web scraping in Python, too many requests sent by the same IP (even by a single external server) can lead to blockages. The solution is to use rotating proxies—change the connection endpoint (and use multiple IPs) in your Python script that is used to make different requests. Rotating different proxy servers by a collection of requests in your Python code makes every single request come from a different place. Possessing a list of proxy servers inside your Python scraper is helpful since each one of your requests is sent with a different IP. This reduces the chances of getting blocked when Python-based scraping at a mass level. In essence, rotating IP addresses is the secret to successful scraping.
Why You Need Rotating Proxies for Web Scraping
The sites are more likely to flag or slow traffic that is seen coming in from a single IP at a very intense level. Using a pool of proxies in Python helps because your requests are spread all over different IP addresses. It looks like different people are making the requests instead of a single person. Rotating between different proxy servers and their IP addresses per request in your Python scraper thus hugely reduces the chances of getting blocked whilst scraping massive data.
Implementing a Simple Proxy Rotation Script: Round-robin vs random rotation
There are two common strategies for rotating proxies:
- Round-robin: cycle through the list in order. Predictable, every proxy gets equal use, easy to debug.
- Random: pick a random proxy each time. Less predictable, harder for the target site to fingerprint your pattern, but proxy usage isn’t evenly distributed.
For most scraping jobs, random is the safer default. Round-robin is fine when you know all your proxies are equally healthy.
```python
import requests
import random
from itertools import cycle
proxy_list = [
"http://user:pass@proxy1.example.com:8080",
"http://user:pass@proxy2.example.com:8080",
"http://user:pass@proxy3.example.com:8080",
]
# --- random ---
def get_random_proxy():
p = random.choice(proxy_list)
return {"http": p, "https": p}
# --- round-robin ---
proxy_pool = cycle(proxy_list)
def get_next_proxy():
p = next(proxy_pool)
return {"http": p, "https": p}
```
Handling dead proxies in your pool
Free and even paid proxies die regularly. A production-ready rotator needs to drop bad proxies on the fly so it doesn’t keep retrying them:
```python
import requests
import random
class ProxyRotator:
def __init__(self, proxies):
self.alive = list(proxies)
self.dead = set()
def get(self):
if not self.alive:
raise RuntimeError("No alive proxies left")
return random.choice(self.alive)
def mark_dead(self, proxy):
if proxy in self.alive:
self.alive.remove(proxy)
self.dead.add(proxy)
rotator = ProxyRotator([
"http://user:pass@proxy1.example.com:8080",
"http://user:pass@proxy2.example.com:8080",
"http://user:pass@proxy3.example.com:8080",
])
for url in urls_to_scrape:
for _ in range(3): # 3 attempts per URL
proxy = rotator.get()
try:
resp = requests.get(url, proxies={"http": proxy, "https": proxy}, timeout=10)
resp.raise_for_status()
break
except (requests.exceptions.ProxyError,
requests.exceptions.ConnectTimeout,
requests.exceptions.SSLError):
rotator.mark_dead(proxy)
else:
print(f"Failed: {url}")
```
In real production you’d want to retry dead proxies after a cool-down (some come back), and you’d want per-proxy success/failure counters to score them. But this is enough for 90% of scraping projects.
When to stop building your own rotator
If you’re hitting more than 50–100k requests per day, managing your own pool starts to cost more in engineering time than it saves. At that scale a rotating residential or ISP proxy endpoint, one single URL where the provider handles rotation internally, is usually the right move. You just point Requests at one address, and every call comes out of a different IP ProxyWing’s rotating proxies work exactly this way.
Using Proxy Pools to Avoid Blocks
Instead of managing proxies yourself in Python, consider a rotating proxy with an IP pool. Such services are in the business of providing many proxies and rotating them, so every request is sent with a new one from the pool. The more there are in your pool, the fewer requests each will get, and thus detection becomes less likely. By distributing requests over many different IP addresses, you vastly reduce the chances of any one of them becoming blocked.
Choosing the Best Proxies for Python Requests

When choosing proxies to use with requests in Python, the following should be considered for effective request routing and anonymity:
• Paid vs. Free: Free proxies are easily accessible but slow, unstable, or already blocked by a very large quantity of sites. Paid service provider sellers have faster and more stable Python-friendly paid proxies. In a situation involving a large Python scraping project or bulk use, good-quality proxies need to be purchased to ensure stable request delivery at scale.
• Datacenter vs. Domestic: Datacenter proxies are cloud or datacenter provider proxies. They are more accessible and faster but are blockable and detectable instantly due to the known addresses. Domestic proxies are ISP (real user connection) proxies and are much harder to identify to sites and are thus suitable for Python-based scraping that involves frequent or high-volume requests. The only drawback is that the Python-ready residential option is costly.
• Ease of use & Authentication: Define what authentication methods are offered by the provider (i.e., username/password basic auth or IP whitelisting) and that it is easy to include the request proxy’s parameter in each outbound request. The easier it is to set up (i.e., all traffic over a single proxy endpoint), the easier your Python development will be.
Troubleshooting Proxy Issues in Python Requests

Despite a proper Python setup, request-handling proxies will still give you issues. Some of the common issues and the corresponding solutions are set out below:
• Connection errors/timeouts: When there is no connection to a request, the connection is either down or not properly set up in your Python code. Try a different server address.
• SSL Certificate Errors: HTTPS tunneling can cause SSL validation errors. As a workaround when debugging in Python, pass verify=False in your call to avoid such errors and to let the request proceed without certificate checks. Use this only when Python-level debugging because this disables the certificate security validation.
By checking these issues and fixing them, you will avoid most issues with proxies in your Python requests and conduct your scraping or API requests with ease.
Conclusion
Python proxies with the request’s library enable you to control your trace IP when making HTTP requests. We have covered configuring setup in requests, authentication credentials, setting up sessions with environmental variables, and IP rotation to avoid getting blocked and maintain request continuity. Touching upon choosing good proxies for request success and debugging, with all those methods you can continue with your Python requests proxy set for web scraping, tests, or anything. Always be ethical with your use of your proxies and stay within the websites that you are targeting with your requests.
Article written by:

Full Stack AI Engineer
Alexandre brings deep full-stack expertise to Proxywing's engineering efforts — from backend architecture and performance optimization to AI-driven development workflows. His hands-on work spans Node.js, React, cloud infrastructure, and RAG pipelines, giving him a rare ability to tackle both proxy platform internals and user-facing product challenges. At Proxywing, Alexandre focuses on designing resilient systems, eliminating performance bottlenecks, and integrating modern AI tooling into the development process. Outside of coding, he's passionate about exploring the frontiers of AI engineering and building side projects that push his technical boundaries.
All articles by author (44)FAQ
Almost always one of three things: the `https` key is missing from your proxies dict, you’re hitting an HTTP-only URL while only the `http` key is set, or an `HTTP_PROXY` environment variable is overriding your in-code config. Set both keys explicitly and run the verification snippet above.
Yes, `pip install requests[socks]`. This pulls in PySocks, which Requests uses under the hood. Without it you’ll get an `InvalidSchema` error when you pass a `socks5://` or `socks5h://` URL.
The proxy is reachable but rejected your credentials. Check the username and password, and URL-encode any special characters (`@`, `:`, `#`, `%`) with `urllib.parse.quote()`. Embedded credentials must be URL-safe.
Yes. The proxies dict takes per-scheme values, so you can route HTTP through one server and HTTPS through another. In practice most providers expose a single endpoint for both, so you’ll usually point both keys to the same URL.
Pass `proxies={“http”: None, “https”: None}` to that call. This overrides any environment-level proxy configuration for that single request. To disable env-proxies for a whole session, set `session.trust_env = False`.
A proxy pool is the list of proxies you have available. Rotation is the strategy of moving between them — round-robin, random, or based on success rate. Some providers also sell a single “rotating endpoint” where the rotation happens server-side and you only see one URL.
For learning and testing, yes. For anything production-grade, no — free proxies are slow, frequently dead, often abused, and many log your traffic. If you’re scraping at any kind of scale, paid residential or ISP proxies pay for themselves in saved engineering time.
Set `session.proxies` once after creating the session, and every request from that session will use it. No need to pass `proxies=` on each call. This is also a TCP connection reuse win — sessions are faster than one-off requests for repeated calls to the same host.



