How to Use a Proxy With Python Requests
Python's requests library speaks HTTP proxies out of the box, so routing it through one takes a two-line dict and nothing to install. This is a copy-paste walkthrough using o2co: a single proxy endpoint — written HOST:PORT below; the real host and port are on your dashboard — with username/password auth. The endpoint speaks HTTP and SOCKS5 on the same port; the snippets use the HTTP form because requests handles it natively. Swap USERNAME and PASSWORD for your real credentials and they run as-is. If proxy protocols are new to you, start with SOCKS5 vs HTTP Proxy.
1. Build the proxies dict
requests takes a proxies dict keyed by scheme. You set both http and https to the same proxy URL — those keys describe the destination scheme, not the proxy protocol. Plain-HTTP targets are forwarded as-is; HTTPS targets are tunneled through a CONNECT request, so the proxy never sees the decrypted traffic:
import requests
PROXY = "http://USERNAME:PASSWORD@HOST:PORT"
proxies = {"http": PROXY, "https": PROXY}
r = requests.get("https://api.ipify.org?format=json", proxies=proxies, timeout=30)
print(r.json()) # -> the proxy's egress IP, not yoursCredentials go inline as USERNAME:PASSWORD@ in the URL; requests turns them into a Basic Proxy-Authorization header for you. If your password contains @, :, or other reserved characters, URL-encode it first with urllib.parse.quote(password, safe="").
2. Verify the egress IP
Never trust a proxy config until you've confirmed traffic actually leaves through it. Make one request without the proxy and one with it, and compare:
import requests
PROXY = "http://USERNAME:PASSWORD@HOST:PORT"
proxies = {"http": PROXY, "https": PROXY}
direct = requests.get("https://api.ipify.org", timeout=30).text
viaproxy = requests.get("https://api.ipify.org", proxies=proxies, timeout=30).text
print("direct :", direct)
print("proxy :", viaproxy)
assert direct != viaproxy, "traffic is NOT going through the proxy"If the two IPs differ, you're routed. For more detail — ASN, country, city — hit https://ipinfo.io/json through the proxy instead and inspect the JSON.
3. Target a country
Egress country is selected through the username, not a separate proxy host or port. Append -country-XX to your username, where XX is a lowercase ISO-3166 alpha-2 code (-country-DE is rejected). That connection then exits through the chosen country:
import requests
def proxies_for(country: str):
user = f"USERNAME-country-{country}"
url = f"http://{user}:PASSWORD@HOST:PORT"
return {"http": url, "https": url}
# Germany this connection, then France the next
de = requests.get("https://ipinfo.io/json", proxies=proxies_for("de"), timeout=30)
fr = requests.get("https://ipinfo.io/json", proxies=proxies_for("fr"), timeout=30)
print(de.json()["country"]) # DE
print(fr.json()["country"]) # FRPlain USERNAME (no suffix) uses the default country saved in your portal, if any. An explicit country your plan doesn't include, or one with no healthy exit right now, fails the connection instead of silently exiting elsewhere. Exit selection happens at the gateway: with the plain username, connections from one machine share an exit while they keep coming, and after about a minute idle the next one may get a new exit, so don't assume every request gets a fresh IP. The one other username switch is -rotate, which combines with the country in either order: USERNAME-country-de-rotate gives every new connection a different German exit. There is no named-session parameter. See proxy geo-targeting and sticky vs rotating sessions for the details.
4. Sessions, timeouts, and error handling
For more than a couple of requests, use a requests.Session. It reuses connections, holds the proxy config once, and is noticeably faster across many calls:
import requests
from requests.adapters import HTTPAdapter
from requests.exceptions import ProxyError, ConnectionError, Timeout
from urllib3.util.retry import Retry
PROXY = "http://USERNAME:PASSWORD@HOST:PORT"
session = requests.Session()
session.trust_env = False # ignore HTTP(S)_PROXY from the shell
session.proxies = {"http": PROXY, "https": PROXY}
retry = Retry(total=3, backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["GET", "HEAD"])
session.mount("http://", HTTPAdapter(max_retries=retry))
session.mount("https://", HTTPAdapter(max_retries=retry))
try:
r = session.get("https://api.ipify.org", timeout=(5, 30))
r.raise_for_status()
print(r.text)
except ProxyError as e:
print("proxy refused or auth failed:", e)
except (ConnectionError, Timeout) as e:
print("network/timeout, retry later:", e)One catch with -rotate: rotation is per connection, not per request, so requests that ride a Session's reused connection keep its exit. For a new IP per request, use a fresh Session (or plain requests.get) per request, or send Connection: close.
Three things matter for reliability:
- Always set a timeout. Without one,
requestscan hang indefinitely on a stalled tunnel. A tuple(connect, read)— here(5, 30)— bounds the handshake and the response separately. - Catch
ProxyErrordistinctly. It usually means bad credentials or a refused upstream, which a blind retry won't fix — whereas aTimeoutorConnectionErroris often worth retrying with backoff. - Retry with backoff, not in a tight loop. The
Retryadapter above backs off on 429 and 5xx and honorsRetry-After. Every retry that gets a response moves bytes, and bytes are what you're billed for.
5. Prefer SOCKS5? Same endpoint, same port
The endpoint also accepts SOCKS5 on the same port, if that is what your codebase standardizes on. Install the extra with pip install "requests[socks]" (without it, socks5h:// raises InvalidSchema) and swap the scheme — everything else, including the country suffix, stays the same. Prefer socks5h:// over socks5:// so DNS resolves at the exit, not on your machine:
PROXY = "socks5h://USERNAME:PASSWORD@HOST:PORT"Common gotchas
- It prints your own IP — the
proxiesdict didn't apply. Check the scheme keys, or that you passedproxies=on the call (not just on a different session). - 407 / auth rejected — URL-encode special characters in
PASSWORD, and confirm the-country-XXsuffix uses a valid lowercase country code (and that-rotate, if used, is lowercase and appears once). - Environment proxies win over
session.proxies— a strayHTTPS_PROXYorNO_PROXYin your shell takes precedence over the session's dict (though not over aproxies=argument on the call). Setsession.trust_env = False, as above.
Next steps
That's the whole pattern: point an http:// proxy URL at the endpoint on your dashboard, verify the egress IP, and pick the country through the username. For curl and browsers, see how to set up a SOCKS5 proxy; for a full crawler, see rotating proxies with Scrapy and our notes on proxies for web scraping. Get 1 GB free to run these snippets against a real endpoint, or see pricing.