Requests is the de-facto Python HTTP library — the one Pythonistas reach for
when they need to talk to a REST API, scrape a web page, download a file, or
hit any HTTP endpoint without wrestling with urllib. It wraps urllib3 with
a clean, expressive API: requests.get(url) returns a Response; the rest of
the surface flows from that single idea.
This guide covers everything an application developer needs to use Requests
effectively: installation, the seven HTTP verb helpers, session management,
authentication, file uploads, streaming, proxies, TLS, and the full exception
taxonomy. It documents version 2.33.1 (built from the 514c162 commit on
the main branch) and targets Python 3.10 or later.
For deep architecture, the transport-adapter contract, hooks internals, and extension points, see the companion Requests Administrator & Developer Guide.
1. Quick Start
Requests provides a Pythonic API for HTTP. You ask for a URL with a verb, you
get a Response object back, you read attributes off it. There is no event
loop, no callback dance, no urlopen() ceremony.
What you get
- Seven verb helpers at the package root —
requests.get,post,put,patch,delete,head,options, plus the genericrequests.request. requests.Sessionfor connection pooling, cookie persistence, and shared auth/headers across many requests.Responseobject with.status_code,.text,.content,.json(),.headers,.cookies,.iter_content(),.iter_lines(),.raise_for_status().- Built-in authentication — Basic, Digest, Proxy-Basic — plus a simple
AuthBasehook for custom schemes. - Automatic JSON encoding/decoding, multipart file uploads, automatic
decompression of gzip/deflate, automatic content-encoding detection
via
charset_normalizer/chardet, and transparent connection reuse throughurllib3's pool manager. - TLS verification on by default with the
certifiCA bundle. - A clean exception hierarchy rooted at
requests.exceptions.RequestException.
Prerequisites
- Python 3.10 or newer. Earlier Pythons will refuse to install at
setup.pylevel: the package raisesRequests requires Python 3.10 or later.and exits. - An internet connection for the install step. Nothing else.
Install
python -m pip install requests
Successfully installed requests-2.33.1
Make your first request
>>> import requests
>>> r = requests.get('https://www.python.org')
>>> r.status_code
200
>>> b'Python is a programming language' in r.content
True
That is the entire pattern. requests.get(url) returns a Response; you read
off whatever attribute you want.
Tip
If you only need one or two requests, the top-level helpers (
requests.get,requests.post, ...) are perfect. If you are about to issue multiple requests against the same host, jump to the Sessions section — sharing aSessionreuses TCP connections and is dramatically faster.
What to try next
- POST a JSON payload:
requests.post(url, json={'key': 'value'}). - Add an auth tuple:
requests.get(url, auth=('user', 'pass')). - Set a timeout:
requests.get(url, timeout=5). - Persist a session:
s = requests.Session(); s.get(...).
2. Installation
System requirements
| Requirement | Minimum | Notes |
|---|---|---|
| Python | 3.10 | Hard floor — setup.py exits with an error on older Pythons. |
urllib3 |
>= 1.21.1 | Installed automatically. The 2.x series is fully supported. |
certifi |
latest | Provides the bundled trust store used by requests.certs.where(). |
charset_normalizer |
>= 2.0, < 4.0 | Default character-set detection. |
chardet |
>= 3.0.2, < 8.0.0 | Optional alternative to charset_normalizer. |
idna |
latest | Used for IDNA hostname encoding (international domain names). |
If both chardet and charset_normalizer are missing at import time, you will
see a RequestsDependencyWarning: "Unable to find acceptable character
detection dependency (chardet or charset_normalizer)." Encoding detection
falls back to UTF-8 in that case.
Important
Requests will refuse to install on Python < 3.10. If you are stuck on an older interpreter, pin to a legacy Requests release; this version (2.33.1) will not run.
Install methods
pip
python -m pip install requests
Poetry
poetry add requests
uv
uv add requests
From source
git clone https://github.com/psf/requests
cd requests
pip install .
Optional companion packages
Requests does not declare its own optional extras in the briefing surface, but the runtime probes for two well-known optional packages and changes behavior when they are present:
- SOCKS proxy support via
urllib3[socks]— installPySocks(or theurllib3SOCKS extra) if you need to route throughsocks5://,socks5h://,socks4://, orsocks4a://proxies. Without it, any attempt to use a SOCKS proxy raisesInvalidSchema('Missing dependencies for SOCKS support.'). - PyOpenSSL fallback — if Python's
sslmodule reportsHAS_SNI=False(a now-rare situation on legacy embedded builds), Requests callsurllib3.contrib.pyopenssl.inject_into_urllib3()to enable SNI via PyOpenSSL. You almost certainly do not need to think about this on modern CPython.
Post-install verification
>>> import requests
>>> requests.__version__
'2.33.1'
>>> requests.get('https://httpbin.org/get').status_code
200
If a request succeeds and you get a 200 back, your install is healthy.
Bug-report helper
Requests ships a tiny module that prints diagnostic information about your
environment — the Python implementation and version, OS, urllib3, OpenSSL,
charset_normalizer/chardet, idna, cryptography, and the Requests
version itself.
python -m requests.help
{
"chardet": {"version": null},
"charset_normalizer": {"version": "3.x.x"},
"cryptography": {"version": "..."},
"idna": {"version": "..."},
"implementation": {"name": "CPython", "version": "3.12.x"},
"platform": {"release": "...", "system": "..."},
"pyOpenSSL": {"openssl_version": "...", "version": "..."},
"requests": {"version": "2.33.1"},
"system_ssl": {"version": "..."},
"urllib3": {"version": "..."},
"using_charset_normalizer": true,
"using_pyopenssl": false
}
Paste this into any GitHub bug report — the maintainers ask for it explicitly.
3. Configuration
Requests is a library, not an application: there is no config file, no
~/.requestsrc, no --config flag. You configure behavior either per-call
(keyword arguments on each request) or per-session (attributes on a Session).
The library does, however, honor a small, well-defined set of environment
variables.
Environment variables Requests reads
| Variable | Read by | Effect |
|---|---|---|
REQUESTS_CA_BUNDLE |
Session.merge_environment_settings() |
Path to a CA bundle used for TLS verification. Overrides the bundled certifi store. |
CURL_CA_BUNDLE |
Session.merge_environment_settings() |
Same role as REQUESTS_CA_BUNDLE; consulted if REQUESTS_CA_BUNDLE is unset. Provides cURL compatibility. |
NETRC |
requests.utils.get_netrc_auth() |
Path to an alternative .netrc file. If unset, Requests checks ~/.netrc then ~/_netrc. |
HTTP_PROXY / HTTPS_PROXY / ALL_PROXY |
via urllib.request.getproxies() |
Default proxies per scheme. Lower-case forms (http_proxy, ...) are also picked up on POSIX. |
NO_PROXY |
via should_bypass_proxies() |
Comma-separated list of hosts/CIDRs/suffixes that bypass proxies. Lower-case no_proxy also works. |
Note
All of the above only fire when
Session.trust_envisTrue(the default). Sets.trust_env = Falseto ignore environment variables entirely — useful in tests, sandboxed processes, or when you want strictly explicit config.
Default headers
Every request, via the default Session, ships with these four headers (case
preserved by CaseInsensitiveDict):
{
'User-Agent': 'python-requests/2.33.1',
'Accept-Encoding': 'gzip, deflate', # exact list comes from urllib3.make_headers
'Accept': '*/*',
'Connection': 'keep-alive',
}
You can override any of these per-request (headers={'User-Agent': '...'})
or once on a session (s.headers.update({'User-Agent': '...'})).
Session attributes you can set
| Attribute | Default | Purpose |
|---|---|---|
headers |
default_headers() |
CaseInsensitiveDict merged into every request. |
auth |
None |
(user, pass) tuple or AuthBase instance applied to every request. |
proxies |
{} |
{scheme: url} mapping. |
params |
{} |
Query-string params merged into every request URL. |
hooks |
{'response': []} |
Response hooks (the only event Requests dispatches). |
verify |
True |
TLS verification: True, False, or path to CA bundle. |
cert |
None |
Client certificate: path to .pem or (cert, key) tuple. |
stream |
False |
If True, response body is not preloaded. |
trust_env |
True |
Honor environment proxies, netrc, and CA-bundle env vars. |
max_redirects |
30 |
Raises TooManyRedirects past this. |
cookies |
empty RequestsCookieJar |
Persisted across requests in the session. |
adapters |
{'http://': HTTPAdapter, 'https://': HTTPAdapter} |
Transport adapters keyed by URL prefix. |
Detailed configuration semantics — every kwarg, every adapter knob, every proxy-resolution rule — live in the Admin Guide.
4. Core Usage
The Requests API is small. There are seven verb helpers and a generic
dispatcher. Every one of them ultimately constructs a Session, builds a
Request, prepares it into a PreparedRequest, dispatches through an
adapter, and returns a Response.
4.1 The seven verb helpers
import requests
requests.get(url, params=None, **kwargs)
requests.options(url, **kwargs)
requests.head(url, **kwargs) # allow_redirects=False by default
requests.post(url, data=None, json=None, **kwargs)
requests.put(url, data=None, **kwargs)
requests.patch(url, data=None, **kwargs)
requests.delete(url, **kwargs)
requests.request(method, url, **kwargs) # generic — accepts any method name
HEAD is the only helper whose default differs from the others: it sets
allow_redirects=False because chasing redirects defeats the purpose of HEAD.
4.2 The kwargs every verb helper accepts
These pass through to Session.request() whichever helper you use:
| Keyword | Type | Purpose |
|---|---|---|
params |
dict / list of 2-tuples / bytes | Query-string parameters. |
data |
dict / list of 2-tuples / bytes / file-like / iterable | Request body (form-encoded if dict). |
json |
any JSON-serializable | Body — sets Content-Type: application/json, calls json.dumps(...). |
headers |
dict | Extra request headers; merged with session headers. |
cookies |
dict / CookieJar |
Cookies to send. |
files |
dict | Multipart form upload — see file uploads section. |
auth |
tuple / AuthBase |
Basic-auth tuple or auth handler. |
timeout |
float / (connect, read) tuple |
Per-phase timeout in seconds. |
allow_redirects |
bool | Defaults to True (False for HEAD). |
proxies |
dict {scheme: url} |
Per-scheme proxy URLs. |
verify |
bool / str | TLS verify: True, False, or CA-bundle path. |
stream |
bool | If True, do not preload body. |
cert |
str / (cert, key) tuple |
Client TLS certificate. |
hooks |
dict {'response': [callable, ...]} |
Hook callbacks. |
4.3 Reading the Response
The object returned by every verb helper is a requests.Response.
r = requests.get('https://api.github.com/repos/psf/requests')
r.status_code # 200
r.reason # 'OK'
r.url # final URL after redirects
r.ok # True if status_code < 400
r.headers # CaseInsensitiveDict
r.headers['content-type'] # 'application/json; charset=utf-8'
r.encoding # 'utf-8' (parsed from Content-Type, or detected lazily)
r.text # body decoded to str
r.content # body as bytes
r.json() # body parsed as JSON, raises JSONDecodeError on bad JSON
r.cookies # RequestsCookieJar
r.history # list of Responses from prior redirect hops
r.elapsed # datetime.timedelta from request send to response headers parsed
r.raw # underlying urllib3 HTTPResponse (only useful with stream=True)
r.links # parsed Link header (for paginated APIs)
r.is_redirect # True if status is in REDIRECT_STATI and Location header present
r.is_permanent_redirect # True for 301 or 308
r.next # PreparedRequest for the next redirect hop, if any
r.request # PreparedRequest that produced this Response
Response is also a context manager (calls .close() on exit) and
truthy-iff-ok (so if r: works).
4.4 Verifying success
raise_for_status() is the canonical way to convert a 4xx/5xx into an
exception:
r = requests.get('https://httpbin.org/status/404')
r.raise_for_status()
# requests.exceptions.HTTPError: 404 Client Error: NOT FOUND for url: https://httpbin.org/status/404
It raises HTTPError for 400-599; for 1xx-3xx it returns None. There is no
side effect.
4.5 Sending a JSON body
import requests
payload = {'name': 'requests', 'stars': 50000}
r = requests.post('https://httpbin.org/post', json=payload)
print(r.json()['json'])
# {'name': 'requests', 'stars': 50000}
Pass json=... and Requests serializes with the standard json module (or
simplejson if installed) and sets Content-Type: application/json. If JSON
serialization fails, you get an InvalidJSONError.
4.6 Sending form data
r = requests.post('https://httpbin.org/post', data={'key1': 'value1', 'key2': 'value2'})
When data is a dict (or list of 2-tuples), Requests form-urlencodes it and
sets Content-Type: application/x-www-form-urlencoded. When data is bytes
or a string, it is sent verbatim with no automatic Content-Type.
4.7 Query-string parameters
payload = {'q': 'requests', 'page': 1}
r = requests.get('https://api.github.com/search/repositories', params=payload)
print(r.url)
# https://api.github.com/search/repositories?q=requests&page=1
Requests handles URL-escaping for you. List values become repeated keys
(?key=a&key=b) — that is, params={'k': ['a', 'b']} produces ?k=a&k=b,
not ?k=a%2Cb.
4.8 Custom headers
r = requests.get(url, headers={'Accept': 'application/vnd.github+json'})
Headers are case-insensitive on read (r.headers['content-type'] works
regardless of how the server cased it), but the case you write is the case
that goes on the wire.
Tip
Requests validates header names and values before sending. Headers with leading whitespace, embedded
\r/\n, or non-string non-bytes values raiseInvalidHeaderfromcheck_header_validity(). This blocks the CRLF-injection class of bugs.
4.9 Timeouts
There is no default timeout. A request without timeout= will hang forever
if the server stops responding. Always set one for production code.
# Single float — applies to both connect and read phases.
requests.get(url, timeout=5)
# Tuple — (connect_timeout, read_timeout) in seconds.
requests.get(url, timeout=(3.05, 27))
Read-timeouts raise ReadTimeout; connect-timeouts raise ConnectTimeout.
Both inherit from Timeout, which inherits from RequestException.
Warning
Passing an invalid timeout (e.g. a 3-tuple) raises
ValueError: Invalid timeout {timeout}. Pass a (connect, read) timeout tuple, or a single float to set both timeouts to the same value.
4.10 Cookies
# Send cookies
r = requests.get(url, cookies={'session': 'abc123'})
# Read cookies the server set
print(r.cookies['session_id'])
# Iterate
for c in r.cookies:
print(c.name, '=', c.value, 'domain=', c.domain, 'path=', c.path)
r.cookies is a RequestsCookieJar — a cookielib.CookieJar subclass that
also speaks the dict interface (['name'], .get('name'), .set('name', 'value')). When two cookies share a name across domains/paths, dict access
raises CookieConflictError with the message
"There are multiple cookies with name, '<name>'" — disambiguate with
.get('name', domain='...', path='...').
4.11 Redirects
Requests follows redirects automatically (except for HEAD). Each hop is
captured in r.history:
r = requests.get('http://github.com') # 301 to https://github.com
r.url # 'https://github.com/'
r.history # [<Response [301]>]
r.history[0].url # 'http://github.com/'
To opt out:
r = requests.get(url, allow_redirects=False)
The redirect chain is bounded by Session.max_redirects (default 30).
Exceeding the limit raises TooManyRedirects with the message
"Exceeded {max} redirects."
Response.is_redirect is True when the status is one of 301, 302, 303, 307, 308 AND a Location header is present. is_permanent_redirect is
True for 301/308.
Note
Requests intelligently strips
AuthorizationandProxy-Authorizationheaders when a redirect crosses hosts, downgrades from HTTPS to HTTP, or changes ports — this prevents leaking credentials to a third party. Thehttp→httpsupgrade on the standard 80→443 port is the one allowlisted exception.
4.12 Streaming responses
For large downloads, set stream=True and consume the body in chunks. The
body is not preloaded into memory until you iterate or touch .content.
with requests.get(url, stream=True) as r:
r.raise_for_status()
with open('file.bin', 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
iter_content(chunk_size=N, decode_unicode=False) yields raw bytes (or str
if you ask). iter_lines(chunk_size, decode_unicode, delimiter) yields one
line at a time — useful for line-delimited APIs like Server-Sent Events.
Caution
iter_lines()is not reentrant-safe. Do not call it twice concurrently on the same Response, and do not interleave it withiter_content().
Warning
Once a streaming body has been fully read, accessing
.contentagain raisesRuntimeError("The content for this response was already consumed"). Reading viaiter_contentafter consumption raisesStreamConsumedError.
Responses are context managers; with requests.get(..., stream=True) as r:
ensures the underlying connection is released back to the pool when the block
exits, even on exception.
4.13 File uploads (multipart)
files = {'report': open('report.csv', 'rb')}
r = requests.post(url, files=files)
Each value can be:
- A file-like object — Requests reads
.read()and guesses a filename fromobj.name. - A 2-tuple
(filename, fileobj). - A 3-tuple
(filename, fileobj, content_type). - A 4-tuple
(filename, fileobj, content_type, custom_headers_dict).
files = {
'logo': ('logo.png', open('logo.png', 'rb'), 'image/png'),
'report': ('report.csv', csv_bytes, 'text/csv', {'X-Note': 'monthly'}),
}
r = requests.post(url, files=files, data={'caption': 'May 2026'})
Mixing data= and files= is supported — form fields ride alongside file
parts in the same multipart body.
Warning
Streamed bodies and files are mutually exclusive. Passing a generator/iterator as
dataand providingfilesraisesNotImplementedError("Streamed bodies and files are mutually exclusive.").
Opening a file in text mode for upload triggers FileModeWarning —
Requests measured the byte length using the file size, but the file is being
read as text, which may produce an incorrect Content-Length on non-ASCII
content. Open with 'rb'.
4.14 Raw streaming bodies (chunked uploads)
If data= is an iterator, generator, or file-like object that is not a
string/list/tuple/Mapping, Requests treats it as a streamed body. If a length
can be determined (super_len() finds it via __len__, len, fileno, or
seek-tell), Content-Length is set; otherwise Requests sets
Transfer-Encoding: chunked.
def gen():
yield b'first chunk'
yield b'second chunk'
requests.post(url, data=gen())
4.15 Response encoding
r.text decodes r.content using r.encoding. The encoding is determined
from the Content-Type header (e.g. text/html; charset=utf-8). If the
header has no charset, r.encoding is None, and r.text falls back to
r.apparent_encoding — charset_normalizer.detect(...)["encoding"] (or
chardet.detect(...), or 'utf-8' if neither library is installed).
Override the encoding manually if the server lies:
r.encoding = 'utf-8'
print(r.text)
5. Sessions
Sessions are the single biggest performance and correctness lever in
Requests. Use a Session whenever you make more than one request to the
same host.
import requests
with requests.Session() as s:
s.headers.update({'User-Agent': 'my-bot/1.0'})
s.auth = ('user', 'pass')
s.params = {'api_version': 'v1'}
r1 = s.get('https://api.example.com/users')
r2 = s.get('https://api.example.com/orders')
5.1 What a Session gives you
- Connection pooling. The default
HTTPAdapterkeeps up to 10 pools, each holding up to 10 keep-alive connections. The next request to the same host reuses an existing TCP+TLS connection — typical 5-10x speed-up versus one-shot calls. - Cookie persistence. Cookies set by the server on one response are
automatically sent on the next request. The session-level
s.cookiesis aRequestsCookieJar. - Default settings. Set
s.headers,s.auth,s.proxies,s.params,s.verify,s.cert,s.hooks,s.stream,s.trust_env,s.max_redirectsonce and they apply to every subsequent request. - Hooks scoped to the session. Register a response hook on the session and every response in that session goes through it.
5.2 The full Session method surface
| Method | Purpose |
|---|---|
Session.request(method, url, **kw) |
Generic dispatcher — verb helpers all delegate here. |
Session.get/options/head/post/put/patch/delete(url, **kw) |
Verb helpers. |
Session.send(prepared, **kw) |
Send an already-prepared request. |
Session.prepare_request(req) |
Build a PreparedRequest from a Request using session defaults. |
Session.merge_environment_settings(url, proxies, stream, verify, cert) |
Apply env-var fallbacks. |
Session.get_adapter(url) |
Return the adapter mounted for the given URL. |
Session.mount(prefix, adapter) |
Register a transport adapter for URLs starting with prefix. |
Session.close() |
Close every mounted adapter (releases pooled sockets). |
5.3 Per-request overrides
Settings on a session are defaults — every call can override them:
s.verify = '/etc/corp/ca.pem'
s.get(url1) # uses corp CA
s.get(url2, verify=False) # disables verification just for this call
The merge rules: dict-typed settings (headers, params, proxies, hooks,
cookies) merge per-request-on-top-of-session; scalar settings (verify,
cert, stream) replace.
5.4 The requests.session() shim
The lowercase requests.session() function returns a new Session for
backward compatibility. It is deprecated since 1.0.0 in the docstring;
prefer the Session class directly.
Tip
Always close your session, either with a
withblock (with requests.Session() as s:) or an explicits.close(). Long-lived processes that forget to close sessions leak file descriptors for the pooled connections.
6. Authentication
Requests ships three built-in auth handlers and exposes a base class for custom schemes.
6.1 Basic authentication
from requests.auth import HTTPBasicAuth
# Long form
r = requests.get(url, auth=HTTPBasicAuth('user', 'pass'))
# Short form — a plain tuple is interpreted as Basic auth
r = requests.get(url, auth=('user', 'pass'))
Both produce Authorization: Basic <base64(user:pass)>. Non-string usernames
or passwords trigger a DeprecationWarning (they will be unsupported in
Requests 3.0.0).
6.2 Digest authentication
from requests.auth import HTTPDigestAuth
r = requests.get(url, auth=HTTPDigestAuth('user', 'pass'))
Digest auth in Requests is thread-local — each thread keeps its own nonce
state, so the same HTTPDigestAuth instance is safe to share across threads.
Algorithms supported: MD5, MD5-SESS, SHA, SHA-256, SHA-512. The
auth=md5 and auth=auth qop values are handled; auth-int is not
implemented.
Note
The Digest implementation registers a response hook on the request to detect a 401 challenge, parse it, and retry with the proper
Authorizationheader. Open issue #6102 reports that non-Latin credentials may misbehave; stick to ASCII credentials for safety.
6.3 Proxy authentication
from requests.auth import HTTPProxyAuth
s = requests.Session()
s.auth = HTTPProxyAuth('user', 'pass') # adds Proxy-Authorization
HTTPProxyAuth derives from HTTPBasicAuth and writes the
Proxy-Authorization header instead of Authorization.
6.4 Custom auth — subclass AuthBase
Any callable that takes a PreparedRequest and returns a PreparedRequest
qualifies as an auth handler. The cleanest pattern is to subclass AuthBase:
from requests.auth import AuthBase
class TokenAuth(AuthBase):
def __init__(self, token):
self.token = token
def __call__(self, r):
r.headers['Authorization'] = f'Bearer {self.token}'
return r
requests.get(url, auth=TokenAuth('eyJhbGc...'))
AuthBase.__call__ is the only required method; the base class raises
NotImplementedError("Auth hooks must be callable.") if you forget.
6.5 .netrc auto-auth
If auth is not set explicitly and Session.trust_env is True, Requests
checks ~/.netrc (or ~/_netrc on Windows, or the NETRC env var) for
credentials matching the request hostname. If a match is found, Basic auth
is applied automatically.
# ~/.netrc
machine api.example.com
login alice
password supersecret
Disable this behavior by setting s.trust_env = False.
7. Common Tasks
Real-world recipes you will hit in practice.
7.1 Talking to a JSON REST API
import requests
API = 'https://api.example.com/v1'
TOKEN = 'sk-...'
with requests.Session() as s:
s.headers.update({'Authorization': f'Bearer {TOKEN}'})
# GET
users = s.get(f'{API}/users', timeout=10).json()
# POST
new = s.post(f'{API}/users', json={'name': 'Ada'}, timeout=10)
new.raise_for_status()
# PATCH
s.patch(f'{API}/users/{new.json()["id"]}', json={'role': 'admin'})
# DELETE
s.delete(f'{API}/users/{new.json()["id"]}')
7.2 Downloading a large file
def download(url, dest, chunk=1 << 14):
with requests.get(url, stream=True, timeout=(5, 60)) as r:
r.raise_for_status()
with open(dest, 'wb') as f:
for block in r.iter_content(chunk_size=chunk):
f.write(block)
download('https://example.com/big.iso', '/tmp/big.iso')
The (5, 60) timeout means: 5s to connect, 60s of inactivity allowed
between data packets.
7.3 Server-Sent Events / line-delimited JSON
import json
with requests.get(url, stream=True, timeout=(5, None)) as r:
for line in r.iter_lines(decode_unicode=True):
if line:
event = json.loads(line)
handle(event)
timeout=(5, None) keeps the read timeout disabled — the connection should
stay open indefinitely as data dribbles in.
7.4 Posting a file
with open('photo.jpg', 'rb') as f:
r = requests.post(url,
files={'photo': ('photo.jpg', f, 'image/jpeg')},
data={'caption': 'sunset'})
7.5 Going through a proxy
proxies = {
'http': 'http://proxy.corp:3128',
'https': 'http://proxy.corp:3128',
}
r = requests.get('https://api.example.com', proxies=proxies)
You can also point per-host:
proxies = {'http://example.com': 'http://proxy1:3128'}
The longer the prefix match, the higher its precedence. SOCKS proxies use
socks5://, socks4://, socks5h://, or socks4a:// schemes; install
urllib3[socks] (PySocks) first.
7.6 Mutual TLS (client certificate)
# Single-file PEM with both cert and key
r = requests.get(url, cert='/etc/ssl/client.pem')
# Separate files
r = requests.get(url, cert=('/etc/ssl/client.crt', '/etc/ssl/client.key'))
7.7 Disabling TLS verification (testing only)
r = requests.get('https://self-signed.example.com', verify=False)
Caution
Setting
verify=Falsemakes your application vulnerable to man-in-the-middle (MitM) attacks. The library docstring states this explicitly: "Only set this toFalsefor testing." In production, pointverify=at your private CA bundle instead.
7.8 Session hooks
Hook every response through a callback for logging or retry logic:
def log_response(r, *args, **kwargs):
print(f'[{r.status_code}] {r.request.method} {r.url} ({r.elapsed.total_seconds():.2f}s)')
with requests.Session() as s:
s.hooks['response'].append(log_response)
s.get('https://httpbin.org/get')
response is the only hook event Requests dispatches; the registered list
runs in order, and any non-None return value replaces the response object
seen downstream.
7.9 Pickling a Response or Session
Response and Session both implement __getstate__/__setstate__. You
can pickle a response only after its content has been fully consumed —
__getstate__ forces .content access first, then drops the live raw
socket. Sessions pickle their stable attributes (headers, cookies,
auth, proxies, ...) but rebuild adapters on load via mount().
7.10 Using Request + prepare() directly
Useful when you want to inspect or mutate the wire bytes before sending:
from requests import Request, Session
req = Request('POST', url, json={'x': 1})
prep = req.prepare()
# inspect or modify
print(prep.headers, prep.body)
with Session() as s:
resp = s.send(prep, timeout=5)
Warning
Session.send()only accepts aPreparedRequest. Passing a plainRequestraisesValueError("You can only send PreparedRequests.")— uses.prepare_request(req)first, or simply calls.request(method, url, ...).
7.11 Looking up status-code names
>>> requests.codes.ok
200
>>> requests.codes.not_found
404
>>> requests.codes.teapot
418
>>> requests.codes['temporary_redirect']
307
requests.codes is a LookupDict where every HTTP status has at least one
human-readable alias; common codes have several (requests.codes.OK,
requests.codes.okay, requests.codes.all_ok, ... all return 200).
8. Troubleshooting
When you see a requests.exceptions.*, the message + status decision tree
below maps to the actual cause.
Quick decision tree
MissingSchema— your URL is missinghttp://orhttps://. The message will literally suggesthttps://{url}.InvalidURL— empty host, invalid IDNA label, bad percent-escape, starts with*or., etc.InvalidSchema— no adapter is mounted for that URL prefix (No connection adapters were found for {url!r}), or a SOCKS proxy was requested withouturllib3[socks]installed.ConnectTimeout— TCP/TLS handshake did not complete in time.ReadTimeout— connection succeeded but no bytes arrived in time.SSLError— certificate verification failed, or no acceptable cert bundle was found.ProxyError— the proxy itself rejected the request.TooManyRedirects— exceededSession.max_redirects(default 30).HTTPError— only raised byr.raise_for_status()on 4xx/5xx.ChunkedEncodingError— server announced chunked transfer but sent malformed chunks.ContentDecodingError—Content-Encodingdeclared gzip/deflate but the body could not be decoded.
All of these inherit from RequestException, which inherits from IOError.
Catch the broadest you can tolerate.
Error: MissingSchema: Invalid URL '...': No scheme supplied. Perhaps you meant https://...?
Cause: You passed requests.get('example.com') — no scheme.
Solution: Prefix with https:// or http://. There is no implicit default.
Prevention: Validate URLs at config time; treat urllib.parse.urlparse(u).scheme
as required.
Error: SSLError: ... certificate verify failed
Cause: The server's certificate chain does not validate against the
trust store Requests is using (usually certifi's bundle).
Solution: If the server uses a private/internal CA, set
verify='/path/to/ca.pem' on the call or set REQUESTS_CA_BUNDLE /
CURL_CA_BUNDLE in the environment. Never ship verify=False to
production.
Prevention: Use a proper PKI; if you must add a corporate root,
distribute it via REQUESTS_CA_BUNDLE.
Error: OSError: Could not find a suitable TLS CA certificate bundle, invalid path: ...
Cause: verify= points at a path that does not exist, or the bundled
certifi data is missing (broken install).
Solution: Reinstall certifi, or correct the path in verify=.
Error: ConnectTimeout / ReadTimeout
Cause: Server unreachable, DNS slow, network issue, or actual server lag.
Solution: Try a longer timeout=. For unreliable networks, use a
custom HTTPAdapter(max_retries=Retry(...)) — see the Admin Guide for the
full retry knobs.
Prevention: Always set timeout= on every external call. Never rely on
Python defaults (there is none).
Error: TooManyRedirects: Exceeded 30 redirects.
Cause: A redirect loop, or a legitimately long chain.
Solution: Inspect r.history for the loop. Bump s.max_redirects
only if you are sure there is no loop.
Prevention: Set allow_redirects=False and follow manually if you do
not trust the upstream.
Error: ChunkedEncodingError
Cause: Server announced Transfer-Encoding: chunked but the body framing
is malformed (truncated, bad chunk-size line, etc.).
Solution: Issue #7341 reports that the documentation around this error is misleading; in practice it almost always indicates an upstream/proxy problem. Retry the request; if it persists, capture the raw bytes with a packet trace and report to the server operator.
Error: InvalidSchema: No connection adapters were found for 'ftp://...'
Cause: Requests only ships HTTP/HTTPS adapters by default. There is no FTP support. (Issue #6711 noted that some AWS S3 SDK paths broke after 2.32.0 changes — make sure you are using a current version.)
Solution: Use a protocol-appropriate library, or mount a custom adapter.
Error: JSONDecodeError from r.json()
Cause: The response body is not valid JSON, or the encoding declaration was wrong and the body could not be decoded into UTF-{8,16,32}.
Solution: Inspect r.text and r.headers['content-type'] first.
Wrap in try/except:
try:
data = r.json()
except requests.exceptions.JSONDecodeError:
print('Bad JSON:', r.text[:200])
Error: RuntimeError: The content for this response was already consumed
Cause: You called iter_content() to drain a streaming response, then
asked for .content. The stream is one-shot.
Solution: Cache the body as you iterate, or skip .content/.text
after streaming.
Error: StreamConsumedError
Cause: Calling iter_content() twice on the same Response after the
content was already consumed (typically because stream=False so the body
was loaded eagerly).
Solution: Don't iterate a non-streamed response. If you need the body,
use r.content or r.text.
Error: UnrewindableBodyError: Unable to rewind request body for redirect.
Cause: The original request body was a non-seekable file or generator, and a redirect required Requests to replay it.
Solution: Pass a seekable file (open(..., 'rb')) or buffer the body
into bytes before sending.
Error: InvalidProxyURL: Please check proxy URL. It is malformed and could be missing the host.
Cause: The proxies={'https': '...'} value is missing a host (e.g.
'http://:3128').
Solution: Fix the proxy URL to include the hostname.
Warning
If you build production code on Requests, treat
RequestException(and ideally each subclass you handle distinctly) as part of your contract. Bareexcept Exception:will mask coding bugs alongside transient network issues.
9. FAQ
Q: How do I disable SSL verification just for one request?
Pass verify=False. Requests will print a single urllib3 warning per
process. Do not use this in production code.
Q: How do I send a list value as a repeated query parameter?
Pass a list: params={'tag': ['python', 'http']} produces
?tag=python&tag=http. If you need a single comma-joined value, join the
list yourself first.
Q: What is the default timeout?
There is none. A request without timeout= waits forever on a stalled
server. Always set one — typical values are 5-30 seconds for connect+read
combined.
Q: Why is my POST data being url-encoded when I want raw JSON?
Use json=..., not data=.... data= form-urlencodes a dict; json=
serializes with json.dumps() and sets Content-Type: application/json.
Q: Where does Requests get its CA bundle?
From certifi — the path is returned by requests.certs.where(). You can
override with REQUESTS_CA_BUNDLE or CURL_CA_BUNDLE env vars, or with
verify='/path/to/bundle.pem' per call/session.
Q: How do I see the exact bytes Requests sent?
Build a PreparedRequest and inspect prep.method, prep.url,
prep.headers, prep.body. To see what comes back, set the standard logging
level on urllib3 to DEBUG:
import logging
logging.basicConfig(level=logging.DEBUG)
Q: Can I share a Session across threads?
Yes — but use one Session per thread for best performance. The connection
pools are thread-safe; the high-level Session machinery is also safe in
practice but contention reduces the value of pooling.
Q: Can I use Requests with asyncio?
Requests is fully synchronous. For async HTTP, use httpx (which has a
similar API) or aiohttp. The PSF maintains Requests as the synchronous
canonical Pythonic HTTP library; there is no requests.aio module.
Q: How do I cancel an in-flight request?
There is no native cancellation. The standard pattern is a short
timeout=. For long downloads, signal a flag and break out of your
iter_content() loop; the underlying socket will be released when the
Response goes out of scope.
Q: Where do I report bugs?
https://github.com/psf/requests/issues.
Include the output of python -m requests.help — the maintainers ask for it.
Q: Does Requests retry failed connections automatically?
By default, no. HTTPAdapter is constructed with max_retries=0. To
enable retries, mount a custom adapter:
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
s = requests.Session()
retries = Retry(total=5, backoff_factor=0.5, status_forcelist=[502, 503, 504])
s.mount('https://', HTTPAdapter(max_retries=retries))
s.mount('http://', HTTPAdapter(max_retries=retries))
See the Admin Guide for the full retry/backoff/status-list reference.
Q: How do I use a custom ssl.SSLContext?
Subclass HTTPAdapter and override build_connection_pool_key_attributes()
to set ssl_context in the pool kwargs. Open issues #5316 and #7040 cover
edge cases here. The Admin Guide walks through the full subclass example.
Q: Why are my non-Latin Digest credentials failing?
Open issue #6102 — HTTPDigestAuth does not handle non-Latin credentials
correctly today. Use Basic auth with explicit UTF-8 encoding, or hold
ASCII-only credentials.
Q: How do I verify my install version at runtime?
>>> import requests
>>> requests.__version__
'2.33.1'
For a full diagnostic dump, python -m requests.help.