Jesse A. Green / notebook

Requests User Guide

Generated April 2026 from psf/requests at 514c162

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

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 generic requests.request.
  • requests.Session for connection pooling, cookie persistence, and shared auth/headers across many requests.
  • Response object 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 AuthBase hook 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 through urllib3's pool manager.
  • TLS verification on by default with the certifi CA bundle.
  • A clean exception hierarchy rooted at requests.exceptions.RequestException.

Prerequisites

  • Python 3.10 or newer. Earlier Pythons will refuse to install at setup.py level: the package raises Requests 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 a Session reuses 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] — install PySocks (or the urllib3 SOCKS extra) if you need to route through socks5://, socks5h://, socks4://, or socks4a:// proxies. Without it, any attempt to use a SOCKS proxy raises InvalidSchema('Missing dependencies for SOCKS support.').
  • PyOpenSSL fallback — if Python's ssl module reports HAS_SNI=False (a now-rare situation on legacy embedded builds), Requests calls urllib3.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_env is True (the default). Set s.trust_env = False to 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 raise InvalidHeader from check_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 Authorization and Proxy-Authorization headers when a redirect crosses hosts, downgrades from HTTPS to HTTP, or changes ports — this prevents leaking credentials to a third party. The http→https upgrade 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 with iter_content().

Warning

Once a streaming body has been fully read, accessing .content again raises RuntimeError("The content for this response was already consumed"). Reading via iter_content after consumption raises StreamConsumedError.

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 from obj.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 data and providing files raises NotImplementedError("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 HTTPAdapter keeps 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.cookies is a RequestsCookieJar.
  • Default settings. Set s.headers, s.auth, s.proxies, s.params, s.verify, s.cert, s.hooks, s.stream, s.trust_env, s.max_redirects once 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 with block (with requests.Session() as s:) or an explicit s.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 Authorization header. 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=False makes your application vulnerable to man-in-the-middle (MitM) attacks. The library docstring states this explicitly: "Only set this to False for testing." In production, point verify= 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 a PreparedRequest. Passing a plain Request raises ValueError("You can only send PreparedRequests.") — use s.prepare_request(req) first, or simply call s.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 missing http:// or https://. The message will literally suggest https://{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 without urllib3[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 — exceeded Session.max_redirects (default 30).
  • HTTPError — only raised by r.raise_for_status() on 4xx/5xx.
  • ChunkedEncodingError — server announced chunked transfer but sent malformed chunks.
  • ContentDecodingError — Content-Encoding declared 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. Bare except 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.