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-filerequests.compatmodule wrapsurllib3import 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}):
Session.post(url, ...)→Session.request('POST', url, json=...).- Build
Request— aRequestinstance is constructed with method, url, json, plus session-aware merges ofheaders,cookies,hooks. Session.prepare_request(req)— produces aPreparedRequest:- merges request and session cookies into a fresh
RequestsCookieJar, - applies
.netrcauth if no explicit auth andtrust_env=True, - calls
PreparedRequest.prepare(), which in turn runsprepare_method,prepare_url(IDNA, validate scheme/host),prepare_headers,prepare_cookies,prepare_body(multipart/json/form/stream),prepare_auth, andprepare_hooks.
- merges request and session cookies into a fresh
Session.merge_environment_settings(url, proxies, stream, verify, cert)— folds in env-var proxies (iftrust_env),REQUESTS_CA_BUNDLE/CURL_CA_BUNDLE, then merges with session defaults viamerge_setting.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
responsehook chain. - Persists cookies via
extract_cookies_to_jar(). - If
allow_redirectsand the response is a redirect, runsSessionRedirectMixin.resolve_redirects(), accumulating intermediate responses intor.history. - If
not stream, eagerly readsr.content(forces full body into memory).
- Picks an adapter via
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)→Responsewithheaders,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_retriesapplies 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
EmptyPoolErrorwarnings 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) inallowed_methodscan 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) oraiohttpfor 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-
Nonevalue, 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 explicittimeout=. - [ ] Long-lived workers use a
Session, not the top-level helpers. - [ ] Session lifetime is bounded (closed in
try/finallyorwith). - [ ] No
verify=Falsein production paths. - [ ] Either
REQUESTS_CA_BUNDLEis unset (usescertifi) or it points at a curated, monitored corporate bundle. - [ ] If retries are enabled,
allowed_methodsexcludes non-idempotent verbs unless idempotency keys are in use. - [ ]
Retry-Afterheader 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.contentif you only need bytes.r.json()if the body is JSON (auto-detects UTF-{8,16,32} via BOM/null pattern inutils.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
6.4 Cookie jar dict access is O(n)
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=Falsedefeats 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://...:443upgrade, 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 raiseInvalidURL. - Demands a scheme — missing schemes raise
MissingSchemawith a hint to prependhttps://. - 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 toInvalidURL("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.
7.6 Cookie security
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
Cookieheader before re-emitting the request to ensure cookies are recomputed against the new URL viaprepare_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__()ofRequest/PreparedRequest/Responsedoes not include the headers or body — only the method or status code. That is safe by default. Butpickle.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 —
HTTPAdapterwith a user-suppliedSSLContextdoes not honor the context'sca_certson Windows. Workaround: setverify=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.pyuses MD5 indirectly viahashlib.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.typedmay 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 orNone. Re-encodes theLocationheader 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 byyield_requests). Handles:- Cumulative redirect history.
TooManyRedirectsaftermax_redirects.- Scheme-less
Locationheaders (//host). - Relative
Locationheaders viaurljoin+requote_uri. - Body removal for non-307/308 (
Content-Length,Content-Type,Transfer-Encodingstripped 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 the301-after-POST → GETbrowser quirk and the302-after-anything-but-HEAD → GETquirk. 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:
- The
proxieskwarg passed to the call. - The Session's
proxies. - Environment variables (
HTTP_PROXY,HTTPS_PROXY, ...) iftrust_env.
should_bypass_proxies(url, no_proxy) checks:
- Explicit
no_proxyargument (orNO_PROXY/no_proxyenv 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()parsesInternet Settingsregistry entries directly to avoid DNS lookups (the stdlibproxy_bypassdoes 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:
- JSON only (
datafalsey,jsonnot None) —json.dumps(json, allow_nan=False), setContent-Type: application/json. - Streaming (
datais iterable but not str/list/tuple/Mapping) — probe length viasuper_len. If known, setContent-Length; else setTransfer-Encoding: chunked. - Multipart (
filesis non-empty) —_encode_files(files, data)produces a multipart body and Content-Type viaurllib3.filepost.encode_multipart_formdata. - Form-urlencoded (
datais a Mapping) —_encode_params(data)produceskey=value&key=value. The body bytes setContent-Lengthautomatically.
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
chardetvscharset_normalizer(preferred order:chardetfirst, thencharset_normalizer). - Re-exports
Mapping,MutableMapping,Callablefromcollections.abc,Morselfromhttp.cookies,cookielibfromhttp.cookiejar, and the legacyurlparse/urlunparse/urljoin/urlencode/quote/unquotefromurllib.parse. - Aliases
simplejson→jsonif installed (socomplexjson.dumpsusessimplejsonwhen 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. Usespytest,pytest-mock, andresponses-style patterns.tests/test_lowlevel.py— wire-format andprepare_*unit tests.tests/test_adapters.py—HTTPAdapterbehavior, 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-filethreading.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 thetests/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_compatibilityandcompat.pyinterrogateurllib3.__version__; the version table and warning categories live in__init__.py. __attrs__for safe pickling.Session,Response, andHTTPAdaptereach declare an__attrs__list of pickleable attribute names; their__getstate__/__setstate__use that list as the contract._internal_utils.pyis the dependency-free leaf. Anything in_internal_utils.pymust not import other Requests modules; thecookies.pydocstring warns "requests.utils imports from here, so be careful with imports".- Backward-compatibility re-exports.
requests.packagesexists so old code doingfrom requests.packages.urllib3 import Xcontinues to work.requests.compatre-exportschardeteven when onlycharset_normalizeris installed. - Default hooks are fresh dicts.
default_hooks()returns a new{event: []}per call so two Requests don't share a hook list. merge_settingskipsNone. Setting a per-request header toNoneremoves 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 meanss.mount('https://api.example.com/', special_adapter)is consulted before the generics.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.typedand full type hints across the public API. - #5498 Use
urllib3.HTTPHeaderDictto avoid a redundant data-conversion when constructing request headers. - #7350 Clarify the
timeout=parameter behavior in docs (especially the(connect, read)tuple semantics andNoneas no-timeout). - #7341 Documentation for
ChunkedEncodingErroris 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
PreparedRequestcannot bypass URL normalization when proxies are used. - #4664
Sessiondoes not handle servers that close keep-alive sockets cleanly — manifests asConnectionErroron 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.yamltoruff 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.6indocs/requirements.txt) matches what RTD builds; bump it in lockstep with their config. - Documentation source is in
docs/; user-facing docs indocs/user/, community process indocs/community/, dev process indocs/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_strhelper inauth.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; useget_connection_with_tls_context).requests.packages— exists for legacy code; new code should importurllib3directly.
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 oneSessionper client and attach a customHTTPAdapterfor retries and TLS. - Auth-handler libraries.
requests-oauthlib,requests-aws4auth, etc. subclassAuthBaseto provide OAuth1/2 and SigV4 signing. - Caching layers.
requests-cacheandcachecontrolmount custom adapters that interceptsend()to serve from a cache. - Mocks.
responses,requests-mock, andhttprettypatch 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.