100% free forever. No signup, no payments. Public proxies are unstable by nature: always test before you trust one.

How to Use a Proxy in Python Requests (With Examples)

The requests library is how most Python developers talk to the web, and routing it through a proxy takes about three lines of code. This guide covers the whole thing: the basic setup, proxies that need a username and password, reusing one setup across many calls, and what to do when a proxy misbehaves (free ones will). Every example below runs as written.

How to Use a Proxy in Python Requests | ProxyNest Blog

The proxies dict: the whole trick

The requests library takes a proxies dictionary. Two keys: http for http:// URLs, https for https:// URLs. The values are the proxy addresses. That is the entire mechanism, and everything else in this guide is a variation on it:

import requests

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

r = requests.get("https://example.com", proxies=proxies, timeout=10)
print(r.status_code)  # 200, wearing somebody else's IP

Set both keys even when they point at the same proxy. If you only set http and then request an https:// URL, requests goes direct and your real IP shows. That is the most common mistake people make with this, so set both every time.

Proxies with a username and password

Paid proxies (and some private ones) want credentials. The format puts them right in the URL: http://username:password@ip:port. Requests builds the auth header for you, so there is nothing extra to code:

import requests

proxies = {
    "http": "http://myuser:[email protected]:8080",
    "https": "http://myuser:[email protected]:8080",
}

r = requests.get("https://example.com", proxies=proxies, timeout=10)
print(r.status_code)

One warning: credentials in the URL are visible to anyone who can read your code or your logs. Keep them in environment variables, not hard-coded strings. And if your password contains characters like @ or :, percent-encode them first, or the URL parser will misread where the address starts.

Use a session when you make many calls

If you are making more than a couple of requests, use a Session. Set the proxies once on the session and every request through it uses them. It also reuses connections, which is faster than opening a fresh one per call:

import requests

session = requests.Session()
session.proxies = {
    "http": "http://203.0.113.45:8080",
    "https": "http://203.0.113.45:8080",
}

r1 = session.get("https://example.com", timeout=10)
r2 = session.get("https://example.org", timeout=10)
print(r1.status_code, r2.status_code)

You can set default headers on the session the same way. For anything beyond a quick one-off script, sessions are the right shape for the job.

Always set a timeout

This one deserves its own section because it bites everyone exactly once. A dead proxy does not politely refuse your connection. It accepts it and then says nothing, forever. Without a timeout, your script hangs until you kill it:

import requests
from requests.exceptions import ConnectTimeout

try:
    r = requests.get("https://example.com", proxies=proxies, timeout=10)
    print(r.status_code)
except ConnectTimeout:
    print("proxy did not answer in time, try another one")

Ten seconds is a sane default. Free proxies are slow, so do not set it to two seconds and conclude everything is dead. Ten gives a working proxy room to breathe and still cuts off the truly dead ones.

Bonus: let environment variables do it

One more way to set proxies that people forget: requests reads the HTTP_PROXY and HTTPS_PROXY environment variables automatically. Set them once in your shell and every requests call in every script picks them up, with no code changes at all:

export HTTP_PROXY="http://203.0.113.45:8080"
export HTTPS_PROXY="http://203.0.113.45:8080"
python my_script.py  # every requests call now uses the proxy

This is handy on servers where the proxy is part of the environment rather than the script. The proxies dict always wins if both are set, so you can still override per request when you need to. One gotcha: some systems also set a lowercase http_proxy, and requests checks the uppercase one first.

Handling the errors you will actually see

Three exceptions cover nearly everything that goes wrong with proxies. ProxyError means the proxy refused the connection or the handshake failed. ConnectTimeout means it never answered. ConnectionError means the connection dropped partway through:

import requests
from requests.exceptions import ProxyError, ConnectTimeout, ConnectionError

def fetch(url, proxies):
    try:
        r = requests.get(url, proxies=proxies, timeout=10)
        r.raise_for_status()
        return r.text
    except ProxyError:
        print("proxy refused the connection")
    except ConnectTimeout:
        print("proxy never answered")
    except ConnectionError:
        print("connection dropped partway through")

A single retry is worth adding, because free proxies fail transiently: a proxy that chokes once often works ten seconds later. Two attempts, not ten. If it fails twice in a row, move on to a different proxy instead of hammering the same dead one:

def fetch_with_retry(url, proxies):
    for attempt in range(2):
        try:
            r = requests.get(url, proxies=proxies, timeout=10)
            r.raise_for_status()
            return r.text
        except (ProxyError, ConnectTimeout, ConnectionError):
            if attempt == 1:
                raise
            print("first try failed, one more go...")

Verify the proxy is actually doing its job

Do not assume the proxy is working just because you got a 200 back. A misconfigured setup can silently go direct. Check which IP the world sees, through the proxy:

import requests

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

r = requests.get("https://api.ipify.org", proxies=proxies, timeout=10)
print("the world sees:", r.text)

If the printed IP matches your proxy's IP, the setup works. If it shows your real IP, the proxies dict is not being applied (check both the http and https keys). You can also test any single proxy in our proxy checker before wiring it into your code, or open our What Is My IP page through the proxy for a full location readout.

What about SOCKS proxies?

Everything above works for HTTP proxies. For SOCKS4 or SOCKS5, install one extra package with pip install requests[socks], then use a socks5:// scheme in the proxy address. The code does not change otherwise:

proxies = {
    "http": "socks5://203.0.113.45:1080",
    "https": "socks5://203.0.113.45:1080",
}

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

If you are not sure which type you need, our HTTP vs SOCKS5 comparison walks through the differences. For SOCKS4, the scheme is socks4:// and everything else stays the same.

Where to get proxies to practice with

You need real proxy addresses for any of this to run. A few options right here: the free proxy list has thousands with copy buttons and country filters, the free API returns them as JSON if you want to pull them into a script, and the proxy checker lets you test any single proxy first. If you want the full rotation setup for scraping work, our scraping with proxies guide builds on everything here.

One last honest note: never send anything sensitive through a free proxy. No logins, no sessions, no personal data. Free proxy operators can see unencrypted traffic. We covered this properly in our safety guide.

About the author

Muhammad Imran runs ProxyNest, a free public proxy directory. He writes plain-English guides about proxies, privacy, and staying safe online.

Comments

Loading comments…

Leave a comment

…