Jesse A. Green / notebook

Requests Administrator & Developer 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.

This document is for the people responsible for embedding Requests into a production system: library integrators, application architects, platform engineers, security reviewers, and the developers who maintain forks or write custom transport adapters. The User Guide tells you how to call requests.get(url). This guide explains what happens after you do — every hook, every adapter knob, every TLS decision, every redirect rule, every extension point.

It documents Requests 2.33.1 at commit 514c162, the current main branch of psf/requests as of 2026-04-15. Where the implementation deviates from the public docs, the source wins; this guide reflects the source.

1. Architecture Overview

Requests is a layered HTTP client. The user-facing API is intentionally tiny (seven verb helpers); the bulk of the implementation is in three classes — Session, PreparedRequest/Response, and HTTPAdapter — that together implement an opinionated wrapper around urllib3.

1.1 The big picture

+-----------------------------------------------------------------+
|  USER CODE                                                      |
|  requests.get(url) / s.post(url, json=...) / etc.               |
+----------------------+------------------------------------------+
                       v
+-----------------------------------------------------------------+
|  requests.api  (api.py)                                         |
|    - get/post/put/patch/delete/head/options/request             |
|    - All delegate to a one-shot Session via a `with` block      |
+----------------------+------------------------------------------+
                       v
+-----------------------------------------------------------------+
|  Session  (sessions.py)                                         |
|    - Holds defaults: headers, cookies, auth, proxies,           |
|      verify, cert, hooks, params, stream, trust_env,            |
|      max_redirects, adapters dict                               |
|    - request() builds a Request, prepares it, sends             |
|    - send() runs hooks, follows redirects, accumulates history  |
|    - SessionRedirectMixin owns redirect logic                   |
+----------------------+------------------------------------------+
                       v
+-----------------------------------------------------------------+
|  Request -> PreparedRequest  (models.py)                        |
|    - Request: user-facing constructor; .prepare() returns PR    |
|    - PreparedRequest: .method/.url/.headers/.body/.hooks        |
|    - prepare_*() methods normalize URL, headers, body, auth,    |
|      cookies, hooks                                             |
+----------------------+------------------------------------------+
                       v
+-----------------------------------------------------------------+
|  Adapter (adapters.py)                                          |
|    - BaseAdapter contract: send(), close()                      |
|    - HTTPAdapter: real impl, owns urllib3 PoolManager and       |
|      ProxyManager registry                                      |
|    - Mounted by URL prefix on Session.adapters (OrderedDict)    |
+----------------------+------------------------------------------+
                       v
+-----------------------------------------------------------------+
|  urllib3.PoolManager / ProxyManager / SOCKSProxyManager         |
|    - HTTPS connection pooling, retries (Retry), keep-alive,     |
|      gzip/deflate decode, chunked transfer                      |
+----------------------+------------------------------------------+
                       v
+-----------------------------------------------------------------+
|  Standard library: http.client, socket, ssl                      |
+-----------------------------------------------------------------+

Note

Requests does not speak HTTP itself. Every byte on the wire is produced by urllib3. Requests' value lies in its API ergonomics, redirect logic, auth handlers, body encoding, encoding detection, hook system, and cookie persistence. This is why a single-file requests.compat module wraps urllib3 import quirks rather than reinventing them.

1.2 Component inventory

Component Path Role
Top-level API src/requests/__init__.py Re-exports the public surface; runs version compatibility check on import.
Verb helpers src/requests/api.py get, options, head, post, put, patch, delete, request. Each opens a one-shot Session() via context manager.
Sessions src/requests/sessions.py Session, SessionRedirectMixin, merge_setting, merge_hooks, deprecated session() shim.
Request/Response models src/requests/models.py Request, PreparedRequest, Response, RequestEncodingMixin, RequestHooksMixin.
Transport adapters src/requests/adapters.py BaseAdapter, HTTPAdapter, urllib3 pool/TLS bridging.
Authentication src/requests/auth.py AuthBase, HTTPBasicAuth, HTTPProxyAuth, HTTPDigestAuth, _basic_auth_str.
Cookies src/requests/cookies.py RequestsCookieJar, MockRequest, MockResponse, cookiejar_from_dict, extract_cookies_to_jar.
Exceptions src/requests/exceptions.py Full hierarchy rooted at RequestException(IOError); warnings rooted at RequestsWarning.
Hooks src/requests/hooks.py HOOKS = ['response'], default_hooks(), dispatch_hook().
Status codes src/requests/status_codes.py codes LookupDict with all aliases.
Data structures src/requests/structures.py CaseInsensitiveDict, LookupDict.
Utilities src/requests/utils.py Proxy resolution, header validation, IDNA helpers, default_headers(), get_netrc_auth(), super_len().
CA bundle src/requests/certs.py where() returns the path to the certifi CA bundle.
Compat shim src/requests/compat.py Reconciles urllib3 1.x vs 2.x and chardet/charset_normalizer.
Internal helpers src/requests/_internal_utils.py to_native_string, unicode_is_ascii, HEADER_VALIDATORS.
Bug-report helper src/requests/help.py python -m requests.help — environment dump.
Package metadata src/requests/__version__.py __title__, __version__='2.33.1', __build__=0x023301, license, author.
Vendored re-exports src/requests/packages.py Compatibility for requests.packages.urllib3.* style imports.

1.3 Technology stack

Layer Library Version constraint Role
HTTP transport urllib3 >= 1.21.1 (2.x supported) Connection pooling, retries, TLS, chunked I/O.
TLS root store certifi latest The CA bundle; requests.certs.where() returns its path.
Charset detection charset_normalizer >= 2.0, < 4.0 Default body-encoding inference.
Charset detection (alt) chardet >= 3.0.2, < 8.0.0 Optional alternative; preferred over charset_normalizer if both are installed.
IDNA idna latest International domain-name encoding for non-ASCII hostnames.
Optional SOCKS urllib3[socks] (PySocks) optional SOCKS proxy support; absence raises InvalidSchema.
Optional PyOpenSSL urllib3.contrib.pyopenssl optional SNI fallback when ssl.HAS_SNI=False.
Build setuptools latest Package builds via setup.py shim that just calls setuptools.setup().

The runtime dependency check runs at import requests time. check_compatibility() in __init__.py asserts urllib3 >= 1.21.1 and a charset-detection library within the supported version range; out-of-range versions trigger RequestsDependencyWarning rather than hard failure.

1.4 Per-request data flow

The flow for s.post(url, json={'k': 1}):

  1. Session.post(url, ...) → Session.request('POST', url, json=...).
  2. Build Request — a Request instance is constructed with method, url, json, plus session-aware merges of headers, cookies, hooks.
  3. Session.prepare_request(req) — produces a PreparedRequest:
    • merges request and session cookies into a fresh RequestsCookieJar,
    • applies .netrc auth if no explicit auth and trust_env=True,
    • calls PreparedRequest.prepare(), which in turn runs prepare_method, prepare_url (IDNA, validate scheme/host), prepare_headers, prepare_cookies, prepare_body (multipart/json/form/stream), prepare_auth, and prepare_hooks.
  4. Session.merge_environment_settings(url, proxies, stream, verify, cert) — folds in env-var proxies (if trust_env), REQUESTS_CA_BUNDLE / CURL_CA_BUNDLE, then merges with session defaults via merge_setting.
  5. Session.send(prepared, **send_kwargs) —
    • Picks an adapter via Session.get_adapter(url) (longest prefix match).
    • Records start = preferred_clock().
    • Calls adapter.send(prepared, stream, timeout, verify, cert, proxies).
    • Records r.elapsed.
    • Dispatches the response hook chain.
    • Persists cookies via extract_cookies_to_jar().
    • If allow_redirects and the response is a redirect, runs SessionRedirectMixin.resolve_redirects(), accumulating intermediate responses into r.history.
    • If not stream, eagerly reads r.content (forces full body into memory).
  6. HTTPAdapter.send() —
    • get_connection_with_tls_context() → urllib3 connection from pool.
    • cert_verify() configures TLS verification.
    • request_url() builds the request-target.
    • add_headers() (no-op by default; subclass hook).
    • conn.urlopen(...) — actual HTTP exchange.
    • Translates urllib3 exceptions into requests.exceptions.*.
    • build_response(req, resp) → Response with headers, status_code, encoding, cookies, request, connection=self.

1.5 Why the one-shot Session in api.py

Every top-level helper does this:

def request(method, url, **kwargs):
    with sessions.Session() as session:
        return session.request(method=method, url=url, **kwargs)

The with block guarantees session.close() runs and pooled sockets are released. Without it, repeated requests.get() calls would leak file descriptors when callers ignore the response. This is also why per-call helpers cannot reuse connections — each call spins up its own pool.

Important

If your code does more than one HTTP call in a single process lifetime, use a Session. The verb helpers exist for one-liners and notebooks.

2. System Requirements

2.1 Runtime requirements

Layer Version Source-of-truth
Python 3.10+ setup.py:if sys.version_info < (3, 10) raises and exits.
urllib3 >= 1.21.1 __init__.py:check_compatibility().
certifi latest requests.certs.where() returns its data path.
charset_normalizer or chardet per matrix above __init__.py:check_compatibility().
idna latest models.PreparedRequest._get_idna_encoded_host.

2.2 Optional runtime extras

Extra Provides Behavior when absent
urllib3[socks] (PySocks) SOCKS4/5 proxies InvalidSchema('Missing dependencies for SOCKS support.').
urllib3.contrib.pyopenssl SNI on legacy CPython Auto-injected only if ssl.HAS_SNI is False.
cryptography OpenSSL backend for PyOpenSSL Old (<1.3.4) versions warn RequestsDependencyWarning.

2.3 Build & test requirements

The repo ships pyproject.toml (PEP 517 build), MANIFEST.in, and Makefile for development tasks. requirements-dev.txt pins development tooling. The docs build uses Sphinx==7.2.6 (pinned in docs/requirements.txt to keep RTD reproducible).

setup.py is a thin shim:

import sys
if sys.version_info < (3, 10):
    sys.stderr.write("Requests requires Python 3.10 or later.\n")
    sys.exit(1)
from setuptools import setup
setup()

All real metadata (project name, version, deps, classifiers) lives in pyproject.toml. The __build__ = 0x023301 integer in __version__.py mirrors the version 2.33.1 in 0xMMmmpp form, used by some downstream tooling for fast version comparison.

3. Configuration Reference

There is no configuration file. All knobs are call kwargs, session attributes, or environment variables.

3.1 Session attributes (full list)

Attribute Type Default Notes
headers CaseInsensitiveDict default_headers() UA, Accept-Encoding, Accept, Connection. Merged per-request.
auth tuple / AuthBase None Applied if request-level auth not set.
proxies dict {} Merged with env proxies if trust_env.
hooks dict {'response': []} Only 'response' event fires.
params dict {} Query string merged into every URL.
verify bool / str True TLS verify mode or CA-bundle path.
cert str / tuple None Client cert (mTLS).
adapters OrderedDict {'https://': HTTPAdapter, 'http://': HTTPAdapter} Sorted by descending prefix length.
stream bool False Per-session default.
trust_env bool True Env vars + netrc only consulted if True.
max_redirects int 30 DEFAULT_REDIRECT_LIMIT from models.py.
cookies RequestsCookieJar empty Persisted across requests.

Session.__attrs__ lists exactly which of these get pickled (everything above except cookies and adapters; cookies are persisted explicitly, adapters are rebuilt on __setstate__ via mount()).

3.2 Environment variables (full list)

Read by Requests itself:

Variable Read by Effect
REQUESTS_CA_BUNDLE Session.merge_environment_settings() Path to CA bundle for TLS verify.
CURL_CA_BUNDLE same Fallback if REQUESTS_CA_BUNDLE unset.
NETRC utils.get_netrc_auth() Override default netrc location.

Read by stdlib urllib.request (and consulted by Requests via get_environ_proxies()):

Variable Effect
HTTP_PROXY, http_proxy Default HTTP proxy.
HTTPS_PROXY, https_proxy Default HTTPS proxy.
ALL_PROXY, all_proxy Default proxy for any scheme.
NO_PROXY, no_proxy Comma-separated bypass list.

Suppressed from honoring environment vars: Session.trust_env = False.

3.3 The full request() kwarg reference

Every kwarg Session.request() accepts (verb helpers all delegate here):

Kwarg Type Default Description
method str required HTTP method. Case-normalized to upper.
url str required Absolute URL. Non-HTTP schemes (e.g. mailto:) skip URL preparation.
params dict / list / bytes / str None Query parameters. Lists yield repeated keys.
data dict / list / bytes / file-like / iterable None Body. Form-encoded if Mapping.
headers dict None Merged with session headers; None-valued keys remove the session value.
cookies dict / CookieJar None Merged into a RequestsCookieJar over session cookies.
files dict None Multipart parts; values are file-like or 2/3/4-tuples.
auth tuple / AuthBase None Replaces session auth.
timeout float / tuple None None = no timeout. Tuple = (connect, read).
allow_redirects bool True (False for HEAD) Follow 301/302/303/307/308.
proxies dict None Per-call proxies; merged with session and env proxies.
hooks dict None One-shot hooks; merged via merge_hooks to preserve session defaults.
stream bool None (falls back to session) If True, body not preloaded.
verify bool / str None (session default) Replaces session value.
cert str / tuple None (session default) Replaces session value.
json any None Auto-serialized to body if data is also empty.

3.4 HTTPAdapter constructor knobs

HTTPAdapter(
    pool_connections=10,    # DEFAULT_POOLSIZE
    pool_maxsize=10,        # DEFAULT_POOLSIZE
    max_retries=0,          # DEFAULT_RETRIES (Retry(0, read=False))
    pool_block=False,       # DEFAULT_POOLBLOCK
)
Kwarg Default Meaning
pool_connections 10 Number of distinct host pools cached by urllib3.PoolManager.
pool_maxsize 10 Max keep-alive connections per pool (per host).
max_retries 0 Either an int (converted via Retry.from_int) or a fully-customized urllib3.Retry. The default builds Retry(0, read=False) — no retries, do not retry idempotent reads.
pool_block False If True, acquire() blocks when no free connection; if False, urllib3 raises.

Most production deployments will mount a custom adapter with a configured Retry policy:

from urllib3.util.retry import Retry
retries = Retry(
    total=5,
    connect=3,
    read=3,
    backoff_factor=0.5,
    status_forcelist=[502, 503, 504],
    allowed_methods=frozenset({'GET', 'HEAD', 'OPTIONS'}),  # urllib3 1.26+ kwarg
    respect_retry_after_header=True,
)
s.mount('https://', HTTPAdapter(max_retries=retries, pool_maxsize=20))

Important

max_retries applies only to failed DNS lookups, socket connections, and connection timeouts. Once data has begun flowing, Requests will not retry. Idempotency is the responsibility of the caller.

3.5 The LookupDict for status codes

requests.codes is a LookupDict populated at import time from the status_codes.py:_codes table. Every code has at least one alias; common codes have many. Lookups are attribute-style (codes.ok) with both lower- and upper-case forms registered (codes.OK), and dictionary-style (codes['ok']). A missing key returns None rather than raising.

The unusual aliases — codes['\\o/'] for 200, codes['-o-'] for 404, codes['/o\\'] for 500 — are present in the source as Easter eggs from the original Kenneth Reitz code base.

4. Deployment & Scaling

Requests is a library, not a service: there is nothing to "deploy" in the operational sense. This section is about the pieces of your application that host Requests-driven HTTP traffic.

4.1 Connection pool sizing

The default HTTPAdapter(pool_connections=10, pool_maxsize=10) is fine for most synchronous applications. Increase pool_maxsize when:

  • A single thread or process makes many concurrent requests to the same host (e.g. a worker pool inside one process).
  • You see urllib3 EmptyPoolError warnings in logs.

Increase pool_connections when you talk to many distinct hosts and notice hosts being evicted from the pool cache.

big = HTTPAdapter(pool_connections=20, pool_maxsize=50, pool_block=False)
s.mount('https://', big)
s.mount('http://',  big)

Tip

One Session per worker thread (or per asyncio loop, when wrapping in loop.run_in_executor) is the simplest model that scales. Sharing one session across hundreds of threads is supported but contention reduces the value of pooling.

4.2 Retries and idempotency

The default adapter does not retry. Mount a custom adapter with a urllib3.Retry to enable backoff:

Retry(
    total=5, backoff_factor=0.5, status_forcelist=[429, 502, 503, 504],
    respect_retry_after_header=True,
    allowed_methods=frozenset({'GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'}),
)

backoff_factor=0.5 produces sleeps of 0.5 * (2 ** (n-1)) seconds — 0.5, 1, 2, 4, ... — between retries.

Warning

Including non-idempotent methods (POST, PATCH) in allowed_methods can cause double-charges, double-sends, or duplicate writes. Default urllib3 behavior excludes them; do not override unless you have idempotency keys.

4.3 DNS, TLS, and process model

Each urllib3 connection caches the resolved IP. Long-lived sessions on hosts with rapidly-changing DNS (e.g. AWS internal LBs) may keep stale addresses. Mitigations: short connection idle timeouts (urllib3-level), or a periodic s.close() to force a fresh pool.

TLS handshakes dominate latency for first-touch requests. Pre-warming a session by issuing one HEAD/OPTIONS at startup is a common trick to avoid the cold-start hit on first user traffic.

4.4 Process forking

Requests Session objects own urllib3 PoolManagers, which own sockets. Forking after creating a session is unsafe — both child and parent will believe they own the same fd. Create sessions after os.fork() (or inside the child entry point of multiprocessing.Process). The same caveat applies to gunicorn pre-fork workers; create the session inside post_fork or lazily on first use within the worker.

4.5 Async / event-loop integration

Requests is synchronous. Calling requests.get() from inside an asyncio coroutine blocks the loop. Two compatible patterns:

  • Wrap calls in asyncio.to_thread() (Python 3.9+):
    await asyncio.to_thread(s.get, url, timeout=5)
    
  • Switch to httpx (semantically similar) or aiohttp for true async.

There is no requests.aio, no requests.async, no AsyncSession. The package surface in __init__.py is exactly the one documented here.

5. Operations

5.1 Logging

Requests itself logs nothing by default — logging.getLogger(__name__).addHandler(NullHandler()) in __init__.py ensures no output. urllib3 is the noisy layer; bump it to DEBUG to see the wire:

import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger('urllib3').setLevel(logging.DEBUG)

5.2 Diagnostic dump for bug reports

python -m requests.help

Output includes Python implementation/version, OS, urllib3, OpenSSL, PyOpenSSL/cryptography, idna, charset_normalizer vs chardet, and the Requests version. The maintainers ask for this on every bug ticket; embed the call into your support tooling.

5.3 Response hooks for telemetry

The response hook fires once per response, including each redirect hop. Wire it to your APM:

def telemetry(r, *args, **kwargs):
    metrics.timing('http.elapsed', r.elapsed.total_seconds(),
                   tags=[f'host:{urlparse(r.url).hostname}',
                         f'status:{r.status_code}',
                         f'method:{r.request.method}'])

s.hooks['response'].append(telemetry)

Hooks must not raise — an exception in a hook propagates out of Session.send() and aborts the request even if the network exchange succeeded.

Note

If a hook returns a non-None value, that value replaces the response seen by everything downstream (subsequent hooks, redirect logic, the caller). Useful for response-rewriting, dangerous if accidental.

5.4 Health checks for outbound dependencies

Requests does not provide a health-check primitive. The conventional pattern is a HEAD against the upstream's documented health endpoint, with a tight timeout:

def healthy(s, url, timeout=2.0):
    try:
        return 200 <= s.head(url, timeout=timeout, allow_redirects=False).status_code < 400
    except requests.RequestException:
        return False

5.5 Operational checklist

  • [ ] Every external requests.* call has an explicit timeout=.
  • [ ] Long-lived workers use a Session, not the top-level helpers.
  • [ ] Session lifetime is bounded (closed in try/finally or with).
  • [ ] No verify=False in production paths.
  • [ ] Either REQUESTS_CA_BUNDLE is unset (uses certifi) or it points at a curated, monitored corporate bundle.
  • [ ] If retries are enabled, allowed_methods excludes non-idempotent verbs unless idempotency keys are in use.
  • [ ] Retry-After header is respected (respect_retry_after_header=True).
  • [ ] Hooks log to telemetry but do not raise.
  • [ ] No sessions created before os.fork() or pre-fork worker spawn.

6. Performance

6.1 Connection reuse is the dominant lever

A single TLS handshake to a typical HTTPS host costs 50-300 ms; keep-alive connection reuse drops subsequent requests to single-digit ms plus server processing. The default pool_maxsize=10 keeps up to ten keep-alive connections per host. Always reuse a Session for any non-trivial workload.

Benchmark pattern (rough order of magnitude on a typical cloud VM hitting a nearby HTTPS endpoint):

Pattern Per-request cost (approx.)
requests.get() per call (one-shot session) TLS handshake every time — typical 50-300 ms.
Session.get() with default pool Handshake once, then ~response-time + RTT.
Session.get() with pool_maxsize=N and N concurrent threads Limited by upstream concurrency / your network.

6.2 Encoding detection has cost

Calling r.text on a response with no charset in Content-Type triggers apparent_encoding, which feeds the entire body through charset_normalizer.detect() (or chardet.detect()). For bodies measured in megabytes, this is significant. Prefer:

  • r.content if you only need bytes.
  • r.json() if the body is JSON (auto-detects UTF-{8,16,32} via BOM/null pattern in utils.guess_json_utf() — much cheaper).
  • Manually setting r.encoding = 'utf-8' if you know the answer.

6.3 prepare_body allocates

PreparedRequest.prepare_body() materializes the entire body unless data= is a stream/generator. For multi-megabyte uploads, pass a file-like object or a generator and let urllib3 chunk the upload:

with open('big.bin', 'rb') as f:
    requests.put(url, data=f)   # streamed; sets Content-Length via super_len

RequestsCookieJar exposes a dict interface for compatibility but cookie storage is a list internally. Operations docstrings warn that "dictionary operations that are normally O(1) may be O(n)". For sessions accumulating hundreds of cookies, prefer iterating directly or using remove_cookie_by_name() instead of del jar[name].

6.5 Header validation is cheap but mandatory

prepare_headers calls check_header_validity() for each header. The two regexes (_VALID_HEADER_NAME_RE_STR, _VALID_HEADER_VALUE_RE_STR) are compiled once at module import; per-header cost is negligible. The check exists to stop CRLF injection, and it is not optional — invalid headers raise InvalidHeader rather than silently corrupting the wire.

7. Security

7.1 TLS verification model

By default, verify=True, and requests.certs.where() returns the certifi bundle path. The bundle is shipped as a certifi package data file and updated on each pip install -U certifi. The actual verification happens in urllib3 via OpenSSL; Requests configures it through HTTPAdapter.cert_verify():

# adapters.py:cert_verify (paraphrased)
if url is HTTPS and verify is True:
    cert_loc = DEFAULT_CA_BUNDLE_PATH                # certifi
elif verify is a path:
    cert_loc = verify
if cert_loc and not os.path.exists(cert_loc):
    raise OSError("Could not find a suitable TLS CA certificate bundle, ...")
conn.cert_reqs = "CERT_REQUIRED"
conn.ca_certs  = cert_loc   (or conn.ca_cert_dir if a directory)

When verify=False, cert_reqs="CERT_NONE" and both ca_certs and ca_cert_dir are unset — the connection accepts any presented certificate.

Caution

Setting verify=False defeats TLS authentication. The session docstring explicitly says: "requests will accept any TLS certificate presented by the server, and will ignore hostname mismatches and/or expired certificates, which will make your application vulnerable to man-in-the-middle (MitM) attacks." This is appropriate for unit tests only — never production.

7.2 Client certificate (mTLS)

verify = True                                 # still verify the server!
cert   = '/etc/ssl/client.pem'                # combined PEM
# or
cert   = ('/etc/ssl/client.crt', '/etc/ssl/client.key')

requests.get(url, verify=verify, cert=cert)

HTTPAdapter.cert_verify() validates that the cert and key paths exist and raises OSError otherwise. Test certificate fixtures live in tests/certs/ (valid CA + server, expired CA + server, mTLS CA + client) and are regenerated via make in those directories.

7.3 Redirect credential stripping

SessionRedirectMixin.should_strip_auth() removes the Authorization header when:

  • the redirect changes hostname, OR
  • the redirect changes scheme, EXCEPT for the standard http://...:80 → https://...:443 upgrade, OR
  • the redirect changes port (other than the default-port case).

Proxy-Authorization is unconditionally removed on redirect (because the new request may go through a different proxy). After stripping, Requests re-applies any .netrc auth that matches the new host (when trust_env=True). This is the intended behavior; it is also why a custom Authorization header set via headers={'Authorization': ...} may disappear on redirect. If you want a header that survives, register an AuthBase instance instead — auth handlers re-run on the redirected request.

7.4 URL validation and IDNA

PreparedRequest.prepare_url():

  • Decodes bytes URLs as UTF-8.
  • Strips leading whitespace.
  • For non-HTTP schemes (mailto:, data:), skips parsing and stores the URL verbatim.
  • Calls urllib3.util.parse_url(); bad parses raise InvalidURL.
  • Demands a scheme — missing schemes raise MissingSchema with a hint to prepend https://.
  • Demands a host — empty hosts raise InvalidURL("Invalid URL '{url}': No host supplied").
  • For non-ASCII hostnames, runs idna.encode(host, uts46=True). IDNA errors are translated to InvalidURL("URL has an invalid label.").
  • Rejects ASCII hosts that start with * or ..
  • Re-percent-encodes the URL via requote_uri().

This pipeline blocks several classes of URL smuggling: empty hosts, wildcard hosts, malformed IDNA labels, and stray whitespace. There is no explicit DNS rebind protection — the IP address is resolved by urllib3 at connection time.

7.5 Header injection blocking

utils.check_header_validity() enforces:

  • Header names match ^[^:\s][^:\r\n]*\Z.
  • Header values match ^\S[^\r\n]*\Z|^\Z (no leading whitespace, no embedded CR/LF).

Either failure raises InvalidHeader before any bytes hit the wire. Header parts must be str or bytes; other types raise InvalidHeader with the message "Header part ({header_part!r}) from {header} must be of type str or bytes, not {type}".

This stops the classic CRLF-injection class of attacks.

RequestsCookieJar inherits from cookielib.CookieJar and uses its default policy. That means standard browser cookie semantics — Domain, Path, Secure, HttpOnly, expiry — are honored. Notably:

  • Secure cookies are only sent over HTTPS.
  • The redirect handler purges the Cookie header before re-emitting the request to ensure cookies are recomputed against the new URL via prepare_cookies.

7.7 Proxy security

The Proxy-Authorization header is never appended to HTTPS-tunneled requests (those go through CONNECT and would leak the credential to the upstream). The check is in SessionRedirectMixin.rebuild_proxies():

if not scheme.startswith("https") and username and password:
    headers["Proxy-Authorization"] = _basic_auth_str(username, password)

When the proxy URL itself contains credentials (http://user:pass@proxy), Requests extracts them via get_auth_from_url() and passes them to the appropriate proxy header — but only on plaintext schemes.

7.8 Secrets handling

Requests does not log credentials, and does not provide any redaction. Application code is responsible for keeping Authorization headers out of its own logs and out of error reports. A useful convention:

def safe_repr(headers):
    h = headers.copy()
    if 'Authorization' in h:
        h['Authorization'] = '<redacted>'
    return dict(h)

Warning

The __repr__() of Request/PreparedRequest/Response does not include the headers or body — only the method or status code. That is safe by default. But pickle.dumps(prepared) will include all headers, credentials and all. Treat pickled requests as secrets.

7.9 Known security-adjacent open issues

Carry these as risks if relevant to your deployment:

  • #5316 — HTTPAdapter with a user-supplied SSLContext does not honor the context's ca_certs on Windows. Workaround: set verify= to the path of the CA bundle as well.
  • #7040 — 2.32.5 changed the way custom SSL contexts are passed; some callers that worked on 2.32.4 broke. Verify your custom context plumbing against your target version.
  • #6810 — auth.py uses MD5 indirectly via hashlib.md5(..., usedforsecurity=False), which is FIPS-tolerant since Python 3.9; the issue tracks completing the FIPS hardening.
  • #7271 — type annotations are RFC'd but not yet inline; scanners expecting py.typed may show no inference.

8. Developer Internals

This section is the "if you fork or extend Requests, here is what you need to know" reference.

8.1 Repository layout

requests/
├── src/requests/                 # all runtime code
│   ├── __init__.py               # public re-exports + dependency check
│   ├── __version__.py            # __version__ = '2.33.1', __build__, ...
│   ├── _internal_utils.py        # to_native_string, HEADER_VALIDATORS
│   ├── adapters.py               # BaseAdapter, HTTPAdapter, urllib3 bridge
│   ├── api.py                    # get/post/put/.../request top-level helpers
│   ├── auth.py                   # AuthBase, HTTPBasicAuth, HTTPDigestAuth, HTTPProxyAuth
│   ├── certs.py                  # where() — certifi bundle path
│   ├── compat.py                 # urllib3 v1/v2 + chardet/charset_normalizer
│   ├── cookies.py                # RequestsCookieJar, MockRequest, MockResponse
│   ├── exceptions.py             # full exception hierarchy
│   ├── help.py                   # python -m requests.help
│   ├── hooks.py                  # HOOKS = ['response'], default_hooks, dispatch_hook
│   ├── models.py                 # Request, PreparedRequest, Response
│   ├── packages.py               # backwards-compat re-exports
│   ├── sessions.py               # Session, SessionRedirectMixin, merge_setting
│   ├── status_codes.py           # codes LookupDict
│   ├── structures.py             # CaseInsensitiveDict, LookupDict
│   └── utils.py                  # default_headers, proxies, netrc, IDNA helpers
├── tests/                        # full test suite (test_requests.py, etc.)
│   ├── certs/                    # mTLS test fixtures (valid, expired, mtls)
│   └── testserver/               # local socket-server harness for integration tests
├── docs/                         # Sphinx user docs (api, install, quickstart, advanced)
├── ext/                          # logo and brand assets (SVG, PNG)
├── pyproject.toml                # PEP 517 build, deps, project metadata
├── requirements-dev.txt          # dev pins
├── setup.py                      # 7-line shim that enforces Python >= 3.10
├── HISTORY.md                    # changelog
├── README.md                     # banner README
├── LICENSE                       # Apache 2.0
└── tox.ini, Makefile, MANIFEST.in

8.2 Key abstractions (cheat-sheet)

Class Inherits Responsibility
BaseAdapter — Abstract: send(), close().
HTTPAdapter BaseAdapter The reference impl over urllib3; the only adapter ships in-tree.
AuthBase — Abstract auth handler; subclass to add auth schemes.
HTTPBasicAuth AuthBase Authorization: Basic.
HTTPProxyAuth HTTPBasicAuth Same but writes Proxy-Authorization.
HTTPDigestAuth AuthBase RFC 7616 digest, thread-local nonce state.
RequestException IOError Root of all Requests exceptions.
RequestsWarning Warning Root of all Requests warnings.
RequestsCookieJar cookielib.CookieJar, MutableMapping Pickleable cookie jar with dict interface.
CaseInsensitiveDict MutableMapping Header dict (preserves original key case, lookups case-insensitive).
LookupDict dict Attribute-access dict (codes.ok); missing keys return None.
Request RequestHooksMixin User-facing constructor; prepare() returns PreparedRequest.
PreparedRequest RequestEncodingMixin, RequestHooksMixin The wire-bytes-ready form of a request.
Response — Server response, with eager/lazy body access.
Session SessionRedirectMixin The connection/cookie/auth/proxy/hook context.

The full inheritance graph (47 classes) lives in extracted/class_hierarchy.tsv; reading it quickly orients you to the exception tree, which is intentionally deep so callers can catch at any granularity (except Timeout catches both connect and read timeouts; except ConnectionError catches ProxyError and SSLError; except RequestException catches everything).

8.3 The exception hierarchy

IOError
└── RequestException
    ├── HTTPError
    ├── ConnectionError
    │   ├── ProxyError
    │   ├── SSLError
    │   └── ConnectTimeout                 (also: Timeout)
    ├── Timeout
    │   ├── ConnectTimeout                 (also: ConnectionError)
    │   └── ReadTimeout
    ├── URLRequired
    ├── TooManyRedirects
    ├── MissingSchema                      (also: ValueError)
    ├── InvalidSchema                      (also: ValueError)
    ├── InvalidURL                         (also: ValueError)
    │   └── InvalidProxyURL
    ├── InvalidHeader                      (also: ValueError)
    ├── ChunkedEncodingError
    ├── ContentDecodingError               (also: urllib3.exceptions.HTTPError)
    ├── StreamConsumedError                (also: TypeError)
    ├── RetryError
    ├── UnrewindableBodyError
    ├── InvalidJSONError
    │   └── JSONDecodeError                (also: json.JSONDecodeError)

Warning
└── RequestsWarning
    ├── FileModeWarning                    (also: DeprecationWarning)
    └── RequestsDependencyWarning

RequestException.__init__ accepts a request= and a response= kwarg so exceptions know which request/response they were attached to — exc.request, exc.response. If only response is passed and it has a request, the request is auto-populated.

8.4 Subclassing HTTPAdapter

The full set of override hooks (all explicitly documented in the source as "only exposed for use when subclassing"):

Method When/why to override
init_poolmanager(connections, maxsize, block, **pool_kwargs) Pass extra kwargs into urllib3 PoolManager (e.g. an ssl_context).
proxy_manager_for(proxy, **proxy_kwargs) Customize per-proxy ProxyManager construction.
cert_verify(conn, url, verify, cert) Override the TLS verification configuration (e.g. force cert pinning).
build_response(req, resp) Customize the Response returned (add custom attrs, etc.).
build_connection_pool_key_attributes(request, verify, cert) Inject ssl_context or other PoolKey attributes.
get_connection_with_tls_context(request, verify, proxies, cert) Replace the connection-acquisition path entirely.
request_url(request, proxies) Customize the request-target (path vs absolute URL).
add_headers(request, **kwargs) Inject extra headers at send time (no-op default).
proxy_headers(proxy) Customize Proxy-Authorization assembly.
send(request, stream, timeout, verify, cert, proxies) Replace the entire send pipeline.

The deprecated get_connection(url, proxies) remains for source-compat with adapters written for Requests <2.32.2. New subclasses should use get_connection_with_tls_context(). The deprecation issues a DeprecationWarning on every call.

Minimal example — pin a specific SSLContext:

import ssl
from requests.adapters import HTTPAdapter

class PinnedAdapter(HTTPAdapter):
    def __init__(self, *args, ssl_context=None, **kw):
        self._ssl_context = ssl_context or ssl.create_default_context()
        super().__init__(*args, **kw)

    def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
        pool_kwargs['ssl_context'] = self._ssl_context
        super().init_poolmanager(connections, maxsize, block, **pool_kwargs)

ctx = ssl.create_default_context(cafile='/etc/corp/ca.pem')
ctx.set_ciphers('ECDHE+AESGCM')
s = requests.Session()
s.mount('https://', PinnedAdapter(ssl_context=ctx))

8.5 The hook system

Defined in hooks.py:

HOOKS = ['response']

def default_hooks():
    return {event: [] for event in HOOKS}

def dispatch_hook(key, hooks, hook_data, **kwargs):
    hooks = hooks or {}
    hooks = hooks.get(key)
    if hooks:
        if hasattr(hooks, '__call__'):
            hooks = [hooks]
        for hook in hooks:
            _hook_data = hook(hook_data, **kwargs)
            if _hook_data is not None:
                hook_data = _hook_data
    return hook_data

There is one — and only one — event today: response. Registering hooks for any other event raises ValueError("Unsupported event specified, with event name {event}") at register_hook(). The kwargs passed to a hook are the same ones used for the request (stream, timeout, verify, cert, proxies). Hooks receive the kwargs so they can build follow-up requests that match the original (this is exactly how HTTPDigestAuth.handle_401 works internally).

8.6 Redirect logic — SessionRedirectMixin

The mixin decomposes redirect handling into:

  • get_redirect_target(resp) — returns the next URL or None. Re-encodes the Location header in latin-1 then re-decodes as utf-8 to handle servers that emit UTF-8 in headers (which is technically out-of-spec but common in practice).
  • should_strip_auth(old_url, new_url) — see Security section above.
  • resolve_redirects(resp, req, ...) — generator yielding either Responses or PreparedRequests (controlled by yield_requests). Handles:
    • Cumulative redirect history.
    • TooManyRedirects after max_redirects.
    • Scheme-less Location headers (//host).
    • Relative Location headers via urljoin + requote_uri.
    • Body removal for non-307/308 (Content-Length, Content-Type, Transfer-Encoding stripped on 301/302/303).
    • Cookie purge before re-prepare.
    • Proxy and auth rebuild via rebuild_proxies / rebuild_auth.
    • Body rewinding via rewind_body() for re-sending.
  • rebuild_method(prepared, response) — implements the 301-after-POST → GET browser quirk and the 302-after-anything-but-HEAD → GET quirk. Per the source comment, "Do what the browsers do, despite standards...".

8.7 Proxy resolution

utils.resolve_proxies(request, proxies, trust_env) is the canonical proxy-decision function. Order of precedence:

  1. The proxies kwarg passed to the call.
  2. The Session's proxies.
  3. Environment variables (HTTP_PROXY, HTTPS_PROXY, ...) if trust_env.

should_bypass_proxies(url, no_proxy) checks:

  • Explicit no_proxy argument (or NO_PROXY/no_proxy env var if not passed).
  • Hostname match against the comma-separated bypass list (suffix matches, CIDRs).
  • Per-platform stdlib proxy_bypass() (consulted on Linux/macOS).
  • On Windows, proxy_bypass_registry() parses Internet Settings registry entries directly to avoid DNS lookups (the stdlib proxy_bypass does reverse DNS, which is slow).

Note

Open issue #2018 tracks a long-standing complaint about proxy precedence ordering. Treat the rules above as the current behavior; do not write code that depends on a particular precedence rule changing in the future.

8.8 Body encoding pipeline

PreparedRequest.prepare_body(data, files, json) decides among four shapes:

  1. JSON only (data falsey, json not None) — json.dumps(json, allow_nan=False), set Content-Type: application/json.
  2. Streaming (data is iterable but not str/list/tuple/Mapping) — probe length via super_len. If known, set Content-Length; else set Transfer-Encoding: chunked.
  3. Multipart (files is non-empty) — _encode_files(files, data) produces a multipart body and Content-Type via urllib3.filepost.encode_multipart_formdata.
  4. Form-urlencoded (data is a Mapping) — _encode_params(data) produces key=value&key=value. The body bytes set Content-Length automatically.

Plain bytes/str data are passed through verbatim — Content-Type is not set automatically; the caller must include it via headers=.

8.9 The compat.py shim

Everything Requests imports from urllib3 or urllib.parse flows through compat.py, which:

  • Detects urllib3 1.x vs 2.x via is_urllib3_1.
  • Resolves chardet vs charset_normalizer (preferred order: chardet first, then charset_normalizer).
  • Re-exports Mapping, MutableMapping, Callable from collections.abc, Morsel from http.cookies, cookielib from http.cookiejar, and the legacy urlparse/urlunparse/urljoin/urlencode/quote/unquote from urllib.parse.
  • Aliases simplejson → json if installed (so complexjson.dumps uses simplejson when available — it is faster on some workloads and understands a broader JSON syntax).
  • Provides legacy aliases basestring = (str, bytes), bytes = bytes, str = str, integer_types = (int,) for source compat with the Python 2 era.

8.10 Testing strategy

  • tests/test_requests.py — the bulk of the integration tests, exercising every public API. Uses pytest, pytest-mock, and responses-style patterns.
  • tests/test_lowlevel.py — wire-format and prepare_* unit tests.
  • tests/test_adapters.py — HTTPAdapter behavior, retries, pool sizing, TLS configuration.
  • tests/test_hooks.py — hook registration, dispatch ordering.
  • tests/test_utils.py — proxy resolution, header validation, IDNA.
  • tests/test_structures.py — CaseInsensitiveDict, LookupDict.
  • tests/testserver/server.py — a tiny single-file threading.Thread-based HTTP server (Server, TLSServer) used for integration tests. It is intentionally minimal: it consumes the request bytes, returns a configurable response, and supports basic TLS and mutual-TLS via the tests/certs/{valid,expired,mtls}/ certificate fixtures.

The cert fixtures are regenerated via make in tests/certs/{valid,expired,mtls}/ which runs openssl req, openssl x509, etc. Expired certs are issued with -days 0 so they are immediately invalid — used to test that SSLError actually fires.

.coveragerc and tox.ini configure multi-Python test runs. CI uses ruff (via .pre-commit-config.yaml: ruff-check --fix, ruff-format) for linting; PRs must pass.

8.11 Code patterns and conventions

  • Single-source-of-truth dependency check. Both __init__.py:check_compatibility and compat.py interrogate urllib3.__version__; the version table and warning categories live in __init__.py.
  • __attrs__ for safe pickling. Session, Response, and HTTPAdapter each declare an __attrs__ list of pickleable attribute names; their __getstate__/__setstate__ use that list as the contract.
  • _internal_utils.py is the dependency-free leaf. Anything in _internal_utils.py must not import other Requests modules; the cookies.py docstring warns "requests.utils imports from here, so be careful with imports".
  • Backward-compatibility re-exports. requests.packages exists so old code doing from requests.packages.urllib3 import X continues to work. requests.compat re-exports chardet even when only charset_normalizer is installed.
  • Default hooks are fresh dicts. default_hooks() returns a new {event: []} per call so two Requests don't share a hook list.
  • merge_setting skips None. Setting a per-request header to None removes the session-level header for that one request — useful for one-shot UA suppression.
  • mount() sorts adapters by descending prefix length. Longest match wins, which means s.mount('https://api.example.com/', special_adapter) is consulted before the generic s.mount('https://', HTTPAdapter()).

8.12 Potential improvements (open RFCs and known gaps)

These are upstream open items as of the briefing snapshot:

  • #7271 RFC: inline type annotations. Adding py.typed and full type hints across the public API.
  • #5498 Use urllib3.HTTPHeaderDict to avoid a redundant data-conversion when constructing request headers.
  • #7350 Clarify the timeout= parameter behavior in docs (especially the (connect, read) tuple semantics and None as no-timeout).
  • #7341 Documentation for ChunkedEncodingError is misleading (the doc says one thing, the code does another).
  • #2018 Re-order proxy precedence — long-standing dispute about whether session, env, or kwargs should win in edge cases.
  • #6830 PreparedRequest cannot bypass URL normalization when proxies are used.
  • #4664 Session does not handle servers that close keep-alive sockets cleanly — manifests as ConnectionError on the next request.
  • #6711 Some AWS S3 SDK paths broke after 2.32.0 changes — possible retry/header behavior regression.
  • #7188 DMTF Redfish fails with 2.32 — interaction between Redfish's custom auth and Requests' redirect handling.

Treat these as known issues; track in your dependency hygiene.

8.13 Contributing

Contributions follow the standard psf/requests workflow:

  • Fork, branch, PR against main.
  • Run pre-commit (ruff-check --fix, ruff-format) before pushing — pinned in .pre-commit-config.yaml to ruff v0.15.9, pre-commit-hooks v6.0.0.
  • Tests live under tests/; new code without tests will not merge.
  • The pinned Sphinx version (Sphinx==7.2.6 in docs/requirements.txt) matches what RTD builds; bump it in lockstep with their config.
  • Documentation source is in docs/; user-facing docs in docs/user/, community process in docs/community/, dev process in docs/dev/.

The project is licensed under Apache 2.0, copyright Kenneth Reitz — the original author — with PSF maintaining since the transition.

9. Integration Points

9.1 What downstream code can reasonably depend on

These are the parts of Requests that are stable, public, and safe to import from external code:

  • requests.{get,post,put,patch,delete,head,options,request}
  • requests.{Session, Request, PreparedRequest, Response}
  • requests.{ConnectionError, ConnectTimeout, FileModeWarning, HTTPError, JSONDecodeError, ReadTimeout, RequestException, Timeout, TooManyRedirects, URLRequired}
  • requests.codes (LookupDict)
  • requests.adapters.{BaseAdapter, HTTPAdapter}
  • requests.auth.{AuthBase, HTTPBasicAuth, HTTPProxyAuth, HTTPDigestAuth}
  • requests.cookies.{RequestsCookieJar, cookiejar_from_dict, extract_cookies_to_jar}
  • requests.exceptions.* (the full exception module)
  • requests.structures.{CaseInsensitiveDict, LookupDict}
  • requests.utils.{default_headers, get_netrc_auth, dict_from_cookiejar, ...}
  • requests.certs.where()
  • requests.help.info() for environment dumps

9.2 What is internal and may break

  • requests._internal_utils.*
  • The _basic_auth_str helper in auth.py (single underscore — private).
  • requests.compat.* is a back-compat shim; new code should use stdlib imports directly (from collections.abc import Mapping, etc.).
  • Methods documented as "This method should not be called from user code, and is only exposed for use when subclassing" — they are stable enough to subclass against, but treat the call surface as a subclass contract, not a public API.
  • HTTPAdapter.get_connection (deprecated; use get_connection_with_tls_context).
  • requests.packages — exists for legacy code; new code should import urllib3 directly.

9.3 Common downstream integrators

Requests is the transport layer for thousands of clients; some patterns worth knowing if you build on it:

  • Session-per-API-client. Most SDKs (the AWS SDK's botocore, the Stripe SDK, the GitHub API SDKs) build one Session per client and attach a custom HTTPAdapter for retries and TLS.
  • Auth-handler libraries. requests-oauthlib, requests-aws4auth, etc. subclass AuthBase to provide OAuth1/2 and SigV4 signing.
  • Caching layers. requests-cache and cachecontrol mount custom adapters that intercept send() to serve from a cache.
  • Mocks. responses, requests-mock, and httpretty patch Requests at the adapter or transport level for testing.

9.4 The deprecation surface

Watch for these DeprecationWarnings in your application logs:

Warning trigger Source Notes
Non-string username/password to HTTPBasicAuth auth.py:_basic_auth_str Will be removed in Requests 3.0.0.
Calling deprecated HTTPAdapter.get_connection() adapters.py Use get_connection_with_tls_context(); required for Requests >=2.32.2 subclasses.
Calling requests.session() (lowercase) sessions.py:session() Deprecated since 1.0.0; use Session().
Opening a text-mode file for upload utils.py:super_len (raises FileModeWarning) Open with 'rb'.
Old cryptography (<1.3.4) __init__.py:_check_cryptography Slowness warning.

Build pipelines should treat DeprecationWarning from Requests as a signal to schedule a fix before the next major bump.