psf/requests
github.com/psf/requests ↗ · 124 files · 1.5 MB
Requests Requests is a simple, yet elegant, HTTP library.
Overview
Requests is the PSF-maintained Python HTTP client library (`v2.34.2`, `src/requests/__version__.py`) that wraps urllib3 in a human-friendly API: `requests.get(...)` returns a `Response` with `.text`, `.json()`, `.cookies`, and `.raise_for_status()`. It is a pure-library distribution — no CLI, no server, no database — whose entire user surface is the seven module-level verb functions in `api.py` plus the `Session` object that persists cookies, auth, proxies, and a urllib3 connection pool across calls. What is distinctive in this commit is how much of the codebase is now type-machinery and hardening rather than new features: a private `_types.py` with `TypedDict` kwargs unpacking, pyright in `strict` mode over `src/requests/`, a pinned/hash-locked GitHub Actions setup audited by zizmor, and an explicit anti-LLM contribution policy. The architecture is deliberately layered so that the transport is swappable: `Session.mount(prefix, adapter)` is the single extension seam that every custom TLS, retry, or mock-transport integration in the ecosystem hangs off.
System map
There is no application entrypoint — the package root `src/requests/__init__.py` is the runtime boundary, and it does dependency-compatibility checking, optional pyOpenSSL SNI injection, and re-export of the public API before anything else runs. Beneath it sit four layers: the stateless verb wrappers (`api.py`), the stateful `Session` (`sessions.py`), the request/response value objects (`models.py`), and the urllib3-backed transport (`adapters.py`). Cross-cutting support lives in `utils.py` (proxies, netrc, header validation, length probing), `cookies.py` (a `CookieJar`/`MutableMapping` hybrid), `structures.py` (`CaseInsensitiveDict`, `LookupDict`), and `exceptions.py`. There is no storage layer: the only persistent state is the in-memory `RequestsCookieJar` on a `Session` and urllib3's connection pools. Build is setuptools via `pyproject.toml`; test/lint/typecheck/publish are four separate GitHub Actions workflows, and distribution is a Trusted-Publishing PyPI upload on `v*` tags.
- The package root is the de-facto entrypoint: importing `requests` runs `check_compatibility()` to assert urllib3 >= 1.21.1 and chardet 3.0.2–8.0.0 / charset_normalizer 2.0.0–4.0.0, emitting `RequestsDependencyWarning` on mismatch rather than failing the import.
- Import also attempts `pyopenssl.inject_into_urllib3()` when `ssl.HAS_SNI` is falsy and then version-checks `cryptography` via `_check_cryptography()`, warning if below 1.3.4 — a side effect that mutates urllib3 globally at import time.
- The public API surface is fixed by the `__all__` tuple in `__init__.py`: 7 verb functions (`get/post/put/patch/delete/head/options`) plus `request`, the `Session`/`Request`/`PreparedRequest`/`Response` types, `codes`, and 9 exception classes.
- `api.request()` is stateless by construction — it opens `sessions.Session()` in a `with` block for every call specifically to close sockets and avoid `ResourceWarning`, meaning module-level calls get zero connection reuse.
- `Session.__init__` establishes all mutable session state (headers from `default_headers()`, empty proxies/params/hooks, `verify=True`, `max_redirects=DEFAULT_REDIRECT_LIMIT` of 30, `trust_env=True`) and mounts an `HTTPAdapter` for both `https://` and `http://`.
- The transport boundary is `HTTPAdapter.send()`, which calls `conn.urlopen(..., redirect=False, preload_content=False, decode_content=False, retries=self.max_retries)` — requests deliberately disables urllib3's own redirect and decode handling and reimplements both.
- The only external integrations are the four hard runtime dependencies declared in `pyproject.toml`: `charset_normalizer>=2,<4`, `idna>=2.5,<4`, `urllib3>=1.26,<3`, `certifi>=2023.5.7`; SOCKS and chardet are optional extras.
- The CA bundle boundary is `certs.where()`, a one-line re-export of `certifi.where` that the module docstring explicitly names as the seam distro packagers should patch; it is captured once at import into `utils.DEFAULT_CA_BUNDLE_PATH`.
- Build/test commands live in the `Makefile`: `make init` installs `requirements-dev.txt`, `make test` runs `pytest tests`, `make ci` adds `--junitxml=report.xml`, and `make coverage` runs pytest-cov against `src/requests`.
- pytest is configured with `addopts = "--doctest-modules"` and `testpaths = ["tests"]`, so the docstring examples in `api.py`, `sessions.py`, and `status_codes.py` are executed as tests alongside the suite.
- Deployment is PyPI-only via `.github/workflows/publish.yml`, which builds with `SOURCE_DATE_EPOCH` pinned to the commit time for reproducibility and publishes through `pypa/gh-action-pypi-publish` with `attestations: true` and `id-token: write` (Trusted Publishing, no stored API token).
- Five CI workflows gate the repo: `run-tests.yml` (matrix of Python 3.10–3.15-dev plus 3.14t free-threaded and pypy-3.11, across ubuntu/macOS/windows), `lint.yml` (pre-commit), `typecheck.yml` (pyright), `codeql-analysis.yml`, and `zizmor.yml` for Actions-security scanning.
Feature catalog
Every user-facing feature is a Python API, not a route or command. The catalog splits into: the seven HTTP verb helpers and their `Session` methods; request-construction features (params, form/JSON/multipart/streamed bodies, file uploads, IDNA hostnames); response-consumption features (`.text` with charset auto-detection, `.json()` with UTF-BOM sniffing, `.iter_content`/`.iter_lines` streaming, `.links` header parsing, `.raise_for_status`); and connection features (pooling, redirects, cookie persistence, proxy resolution including SOCKS, TLS verification and mTLS client certs, netrc auth, digest auth). Two auxiliary surfaces exist: `python -m requests.help` prints a JSON environment report for bug filing, and `python -m requests.certs` prints the CA bundle path.
- Seven verb helpers are exposed at module level and mirrored as `Session` methods; `head()` uniquely defaults `allow_redirects` to `False` at both levels while `get`/`options` default it to `True`.
- Request bodies support four distinct shapes decided in `prepare_body`: JSON via `complexjson.dumps(json, allow_nan=False)` → `application/json`; multipart via `_encode_files`; form-encoding via `_encode_params` → `application/x-www-form-urlencoded`; and streamed iterables/file objects that set `Transfer-Encoding: chunked` when length is unknown.
- Multipart uploads accept 2-, 3-, and 4-tuples (`filename, fileobj`, `+content_type`, `+custom_headers`) and are assembled into urllib3 `RequestField` objects by `encode_multipart_formdata`.
- Streaming bodies and file uploads are mutually exclusive — combining them raises `NotImplementedError("Streamed bodies and files are mutually exclusive.")`.
- Internationalised domains are supported by `_get_idna_encoded_host()`, which applies `idna.encode(host, uts46=True)` for any non-ASCII host and raises `InvalidURL("URL has an invalid label.")` on failure.
- `Response.text` decodes using the header-declared charset, falling back to `apparent_encoding` (charset_normalizer or chardet), and finally to `utf-8` with `errors="replace"` if no detection library is installed.
- `Response.json()` sniffs UTF-8/16/32 via `guess_json_utf()` when no encoding is declared and the body exceeds 3 bytes, and normalises every decode failure into `requests.exceptions.JSONDecodeError`.
- Streaming download is offered by `iter_content(chunk_size, decode_unicode)` and the line-oriented `iter_lines(delimiter=...)`; re-iterating a consumed non-streamed response raises `StreamConsumedError`.
- Pagination via RFC 5988 is supported by `Response.links`, which parses the `Link` header through `parse_header_links()` and keys results by `rel` (falling back to `url`).
- `requests.codes` is a `LookupDict` supporting attribute and item access with multiple aliases per code — including the joke aliases `codes['\\o/']` and `codes['✓']` for 200 — generated by `_init()` which also appends the full table to the module docstring.
- Three auth handlers ship: `HTTPBasicAuth`, `HTTPProxyAuth` (same header under `Proxy-Authorization`), and `HTTPDigestAuth` supporting MD5, MD5-SESS, SHA, SHA-256 and SHA-512 with `qop=auth`.
- `.netrc` credentials are honoured automatically when `trust_env` is true, checked at `$NETRC` if set otherwise `~/.netrc` and `~/_netrc`, and re-applied on cross-host redirects by `rebuild_auth`.
Data flow
Input arrives as loosely-typed Python kwargs at `api.request()` or `Session.request()`, is funnelled into a `Request` value object, then normalised into a fully-serialised `PreparedRequest` (bytes body, `CaseInsensitiveDict` headers, percent-encoded URL string) by `Session.prepare_request()`. Session-level and environment-level settings are merged in at two distinct points — `merge_setting`/`merge_hooks` during preparation, and `merge_environment_settings` for proxies/verify/cert/stream just before send. The `PreparedRequest` crosses the adapter boundary into urllib3, and the urllib3 response crosses back through `HTTPAdapter.build_response()`, which is the sole constructor of `Response` objects. State ownership is clean: `Session` owns cookies, adapters and defaults; `PreparedRequest` owns the wire representation; `Response` owns `_content`/`_content_consumed` and lazily pulls bytes off `raw`. Output boundaries are the `Response` accessors and the environment mutations in `should_bypass_proxies` (via `set_environ`) and `extract_zipped_paths` (via `tempfile.mkstemp`).
- Settings precedence is implemented by `merge_setting(request_setting, session_setting)`: non-Mapping request values win outright, Mappings are merged with request keys overriding, and any key explicitly set to `None` is deleted from the merged result — that is how callers suppress a session default header.
- Hooks need special merging because `{'response': []}` would otherwise wipe session hooks; `merge_hooks` short-circuits when either side has an empty response list.
- Cookies flow through a three-way merge in `prepare_request`: `merge_cookies(merge_cookies(RequestsCookieJar(), self.cookies), request.cookies)`, so per-request cookies layer over session cookies into a fresh jar rather than mutating the session's.
- `PreparedRequest.prepare()` runs a fixed six-step pipeline — method, url, headers, cookies, body, auth — then hooks; the ordering comment states auth must be last so schemes like OAuth can sign a fully-formed request and still register hooks.
- `prepare_auth` mutates the prepared request by `self.__dict__.update(r.__dict__)` from whatever the auth callable returns, then recomputes Content-Length — so an auth handler can legitimately rewrite body and headers.
- Credentials embedded in the URL are extracted into auth during preparation: `prepare_auth` calls `get_auth_from_url(self.url)` when no explicit auth was supplied, and `prepare_url` reassembles netloc with the `auth@` prefix preserved.
- Environment enters the flow at `merge_environment_settings`, which (only when `trust_env`) seeds proxies from `getproxies()` and overrides `verify` from `REQUESTS_CA_BUNDLE` or `CURL_CA_BUNDLE` — explicitly to be cURL-compatible.
- Proxy selection is layered: `resolve_proxies` consults `should_bypass_proxies` (NO_PROXY, CIDR matching, platform `proxy_bypass`), then `select_proxy` picks by the key precedence `scheme://host` → `scheme` → `all://host` → `all`.
- Body length determination flows through `super_len()`, which probes `__len__`, `.len`, `fileno()`+`os.fstat`, and `seek/tell`, subtracting the current file position so partially-read files report the remaining bytes.
- Header values are validated at the input boundary by `check_header_validity`, which regex-matches against `HEADER_VALIDATORS` to reject CR/LF and leading-whitespace — the CRLF-injection guard.
- The response side is constructed exclusively in `HTTPAdapter.build_response()`, which lifts status, `CaseInsensitiveDict` headers, encoding from `get_encoding_from_headers`, `raw`, reason, url, extracts server cookies into `response.cookies`, and back-links `request` and `connection`.
- Cookies persist onto the Session after the fact: `Session.send` calls `extract_cookies_to_jar(self.cookies, request, r.raw)` for the final response and separately for every entry in `r.history`.
Behavior traces
Three flows carry most of the library's behaviour: the one-shot `requests.get()` path, the redirect-following loop inside `Session.send`, and the digest-auth 401 retry. All three converge on `Session.send` → `HTTPAdapter.send` → `build_response`. The redirect loop is a generator (`resolve_redirects`) that is fully drained inside `send`, which is why history arrives pre-assembled on the returned `Response`. The digest flow is unusual in that it re-enters the transport from a response hook (`r.connection.send(prep)`), bypassing `Session.send` entirely.
- Import trace: `requests/__init__.py` runs `check_compatibility` → optional `pyopenssl.inject_into_urllib3` → `warnings.simplefilter('ignore', DependencyWarning)` → re-exports → `logging.getLogger(__name__).addHandler(NullHandler())` → `simplefilter('default', FileModeWarning, append=True)`.
- One-shot GET trace: `api.get()` → `api.request()` opens `Session()` in a `with` → `Session.request()` → builds `Request` → `Session.prepare_request()` → `merge_environment_settings()` → `Session.send()` → returns, and `__exit__` calls `Session.close()` closing every adapter.
- Preparation trace: `prepare_request` → `PreparedRequest.prepare()` → `prepare_method` (uppercases) → `prepare_url` (IDNA + `requote_uri(urlunparse(...))`) → `prepare_headers` (`check_header_validity` per header) → `prepare_cookies` (sets `Cookie` header once) → `prepare_body` → `prepare_auth` → `prepare_hooks`.
- Send trace: `Session.send` defaults stream/verify/cert, rejects a bare `Request` with `ValueError("You can only send PreparedRequests.")`, resolves the adapter by longest matching prefix via `get_adapter`, times the call with `preferred_clock` into `r.elapsed`, then dispatches the `response` hook.
- Adapter trace: `HTTPAdapter.send` → `get_connection_with_tls_context` → `cert_verify` (sets `conn.ca_certs`/`cert_file`, raising `OSError` if any path is missing) → `request_url` (full URL for proxied HTTP, path-only otherwise) → `add_headers` (no-op hook) → `conn.urlopen(...)` → `build_response`.
- Redirect trace: `Session.send` calls `resolve_redirects(r, request)` and drains the generator into `history`; the generator per hop consumes `resp.content`, checks `len(resp.history) >= self.max_redirects` raising `TooManyRedirects`, calls `resp.close()`, normalises the Location, and copies the request.
- On a non-307/308 redirect the loop strips `Content-Length`, `Content-Type` and `Transfer-Encoding` and blanks `prepared_request.body`, then always pops the `Cookie` header before re-deriving it from the jar.
- Method rewriting per hop is `rebuild_method`: 303 → GET (unless HEAD), 302 → GET (unless HEAD, 'what browsers do, despite standards'), and 301 on a POST → GET.
- Auth rewriting per hop is `rebuild_auth` → `should_strip_auth`, which drops `Authorization` on any hostname change but deliberately preserves it for http:80 → https:443 on the same host for backwards compatibility.
- Body rewind trace: a redirect with `_body_position` set and a length header triggers `rewind_body(prepared_request)`; a prior failed `tell()` stores a sentinel `object()` so the rewind raises `UnrewindableBodyError` instead of hanging the connection.
- Non-following trace: when `allow_redirects=False`, `Session.send` primes `r._next` by taking one item from `resolve_redirects(..., yield_requests=True)` inside a `try/except StopIteration`, which is what backs `Response.next`.
- Digest trace: `HTTPDigestAuth.__call__` registers `handle_401` and `handle_redirect` as response hooks and records `r.body.tell()`; on a 4xx `handle_401` parses `www-authenticate`, consumes and closes the response, copies the request, builds the `Authorization` header and re-sends via `r.connection.send(prep, **kwargs)`, appending the original to `_r.history`.
Testing signals
Testing is pytest-only, concentrated in one 108 KB file: `tests/test_requests.py` holds 237 of the ~347 test functions, with `tests/test_utils.py` (64) and `tests/test_lowlevel.py` (13) next. Integration tests run against a real local `httpbin` via the `pytest-httpbin` fixture (wrapped by `conftest.prepare_url` to guarantee a trailing slash), while wire-level behaviour uses a hand-rolled socket `Server` thread in `tests/testserver/server.py`. TLS behaviour is covered by checked-in fixture certificates (`tests/certs/{valid,expired,mtls}`) plus a `trustme`-generated no-SAN server fixture. Doctests in the library modules are executed by `--doctest-modules`. Gaps: `adapters.py` — the single most security-relevant module — has a one-test dedicated file, and there is no coverage threshold enforced anywhere.
- The framework is pytest with `minversion = 6.2`, `testpaths = ["tests"]`, and `--doctest-modules` plus `NORMALIZE_WHITESPACE ELLIPSIS` optionflags, so module docstring examples are part of the suite.
- Test distribution is extremely lopsided: `tests/test_requests.py` defines 237 `test_` functions across 7 classes (`TestRequests`, `TestCaseInsensitiveDict`, `TestMorselToCookieExpires`, `TestMorselToCookieMaxAge`, `TestTimeout`, `RedirectSession`, `TestPreparingURLs`), versus 64 in `test_utils.py` and 13 in `test_lowlevel.py`.
- An autouse fixture `clean_proxy_environ` monkeypatches away `http_proxy`, `https_proxy`, `no_proxy`, `ftp_proxy` and `all_proxy` (both cases) for every test, so proxy tests must set env vars explicitly.
- Integration coverage runs against a live httpbin server through the `httpbin`/`httpbin_secure` fixtures, pinned via `pytest-httpbin==2.1.0` and `httpbin~=0.10.0` in the `test` dependency group.
- Wire-level tests use a purpose-built threaded socket server with `text_response_server` / `basic_response_server` helpers, exercising chunked upload, conflicting Content-Length, RFC 1808 non-ASCII redirects and fragment handling.
- Digest auth is covered at the socket level by three dedicated tests: `test_digestauth_401_count_reset_on_redirect`, `test_digestauth_401_only_sent_once`, and `test_digestauth_only_on_4xx`.
- TLS paths are tested with committed fixture certs — expired server cert, valid server cert, and an mTLS client cert — including `test_different_connection_pool_for_mtls_settings` verifying pool-key separation.
- A `nosan_server` fixture mints a CA and a commonName-only (no subjectAltName) certificate at runtime using `trustme`, backing the SNI/SAN warning tests.
- `test_response_iter_lines_reentrant` is marked `@pytest.mark.xfail` and documents a known defect: `Response.iter_lines()` is not reentrant-safe, matching the docstring note on the method itself.
- Behaviour differences between urllib3 1.x and 2.x are handled by skips keyed on `is_urllib3_1`, e.g. `test_content_length_for_string_data_counts_bytes` skips on 1.x because it encodes strings as latin-1 rather than utf-8.
- `adapters.py` has a single dedicated regression test (`test_request_url_handles_leading_path_separators`); the rest of the adapter is exercised only indirectly through `test_requests.py`.
- CI runs `make ci` (`pytest tests --junitxml=report.xml`) across 8 Python versions × 3 OSes, plus two special jobs: `no_chardet` uninstalls both detection libraries, and `urllib3` pins `urllib3<2`.
Risk map
As a library in ~4M dependent repos, the blast radius of any change here is close to maximal, and the highest-risk surfaces are the ones that decide whether credentials cross a trust boundary: `should_strip_auth`, `rebuild_proxies`, `prepare_url`, and `cert_verify`. Several behaviours are knowingly non-ideal and preserved for backwards compatibility — the http→https auth-retention carve-out, `_basic_auth_str` accepting non-strings, and `LookupDict.__getitem__` silently returning `None`. Environment trust is on by default (`trust_env=True`), so `.netrc`, `$NETRC`, `REQUESTS_CA_BUNDLE`/`CURL_CA_BUNDLE` and `*_proxy` variables all influence behaviour implicitly. Filesystem and process-global side effects exist in `extract_zipped_paths`, `atomic_open`, `set_environ`, and the import-time `pyopenssl.inject_into_urllib3()`.
- `should_strip_auth` intentionally keeps the `Authorization` header across an http:80 → https:443 redirect on the same host; the comment states this is not RFC 7235 behaviour and is retained only for backwards compatibility.
- `rebuild_proxies` reconstructs `Proxy-Authorization` from proxy URL credentials but skips it when the target scheme starts with `https`, explicitly to avoid leaking the header into a TLS tunnel — a subtle invariant easy to break in a refactor.
- Any URL containing a colon whose scheme does not start with `http` bypasses all URL preparation and is assigned verbatim to `self.url`, so `mailto:`/`data:`-style inputs skip IDNA, requoting and host validation entirely.
- `verify=False` disables certificate and hostname validation at the urllib3 level (`cert_reqs = 'CERT_NONE'`, ca_certs cleared); the Session docstring is explicit that this exposes callers to MitM and is for testing only.
- `trust_env=True` by default means an attacker-controlled or accidentally-set `REQUESTS_CA_BUNDLE`/`CURL_CA_BUNDLE` silently replaces the trust store for every request on the session.
- `get_netrc_auth` reads credentials from a path taken from the `$NETRC` environment variable without restriction, and swallows `NetrcParseError`/`OSError` unless `raise_errors=True` — failures to read credentials are silent by default.
- `extract_zipped_paths` performs file IO outside the repo root: it walks a path upward looking for a zip archive, then extracts a member into a `tempfile.mkstemp()` file that is never cleaned up, returning that path to the caller.
- `_basic_auth_str` still accepts non-string usernames/passwords, coercing them with a `DeprecationWarning` promising removal in 3.0.0; the source comment preserves it precisely because callers depend on it.
- Digest auth uses MD5 by default when the server omits an `algorithm` directive; MD5/SHA-1 usage is flagged `usedforsecurity=False` to appease FIPS builds but the weak-hash negotiation is server-driven.
- `handle_401` re-sends the authenticated request directly through `r.connection.send(prep, **kwargs)`, bypassing `Session.send` — so session-level cookie extraction, hooks, and redirect resolution do not run for that hop.
- `prepare_auth` blanket-applies `self.__dict__.update(r.__dict__)` from a user-supplied callable, meaning a third-party auth class can overwrite any attribute of the PreparedRequest including `url` and `hooks`.
- `LookupDict.__getitem__` deliberately falls through to `None` for unknown keys, so a typo'd `requests.codes['okey']` silently yields `None` instead of raising — a footgun with no test-enforced guard.
Extension points
There is one primary extension seam and several secondary ones. The primary is the Transport Adapter: `Session.mount(prefix, adapter)` keeps `self.adapters` sorted by descending prefix length and `get_adapter` picks the longest matching prefix, so any URL prefix can be routed to a `BaseAdapter` subclass — this is how retry policies, custom TLS contexts, and test doubles are injected. Secondary seams are the response hook system, the `AuthBase` callable protocol, the `SessionRedirectMixin` methods documented as overridable, and the packager-facing `certs.where()`. Nearly every private-looking `HTTPAdapter` method carries a docstring saying it exists specifically for subclass overrides.
- `Session.mount(prefix, adapter)` is the registry: it inserts into the `self.adapters` OrderedDict then re-pops every shorter key so adapters stay in descending prefix-length order.
- `Session.get_adapter(url)` resolves by case-insensitive `startswith` over that ordered map, raising `InvalidSchema(f"No connection adapters were found for {url!r}")` when nothing matches — so registering a new scheme is purely a mount call.
- `BaseAdapter` defines the minimal contract — `send(request, stream, timeout, verify, cert, proxies)` and `close()` — that any replacement transport must implement.
- Eight `HTTPAdapter` methods are documented as subclass-override points rather than user API: `init_poolmanager`, `proxy_manager_for`, `cert_verify`, `build_response`, `get_connection_with_tls_context`, `request_url`, `add_headers`, and `proxy_headers`.
- `HTTPAdapter.add_headers()` is an intentional empty hook — 'As of v2.0 this does nothing by default, but is left for overriding by users that subclass' — and is called on every send with the full kwargs.
- `init_poolmanager(**pool_kwargs)` forwards arbitrary kwargs to `urllib3.PoolManager`, which is the documented way to inject a custom `ssl_context` or TLS version.
- The hook registry is the `HOOKS = ['response']` list in `hooks.py`, with `default_hooks()` building `{event: []}` and `dispatch_hook()` chaining callables so each non-None return replaces the data for the next hook.
- Hooks can be attached per-request (`hooks=` kwarg), per-session (`Session.hooks`), or programmatically via `RequestHooksMixin.register_hook` / `deregister_hook`, which both `Request` and `PreparedRequest` inherit.
- Custom auth plugs in via `AuthBase`, whose `__call__(self, r) -> PreparedRequest` raises `NotImplementedError` by default; `prepare_auth` treats any non-2-tuple auth value as such a callable.
- `SessionRedirectMixin` is a separately-inheritable class exposing `get_redirect_target`, `should_strip_auth`, `resolve_redirects`, `rebuild_auth`, `rebuild_proxies` and `rebuild_method` — the tests subclass it directly as `RedirectSession` to exercise redirect logic without a network.
- `get_redirect_target`'s docstring explicitly invites a custom mixin that caches the redirect location on the response as a private attribute, acknowledging the method is called redundantly per hop.
- `certs.where()` is the packager extension point: the module docstring instructs distributors to change its definition to return a separately packaged CA bundle.
Contributor handoff
Treat this repo as a stability-first, extremely high-blast-radius library: the maintainers protect version, changelog, workflows and pyproject behind CODEOWNERS, and the project has a written policy banning unsupervised agentic contributions. Read `sessions.py`, `models.py` and `adapters.py` before touching anything; almost every non-obvious branch in them encodes a specific GitHub issue number that you should look up before 'simplifying' it. Local loop is `make init` then `make test`; before pushing, run `pre-commit run --all-files` and `pyright src/requests/`, since lint and typecheck are separate required CI gates. The biggest trap is that `tests/` is excluded from pyright but `src/requests/` is checked in `strict` mode, so type annotations in library code must be complete and the codebase is dense with deliberate `# type: ignore` comments carrying explanatory reasons — preserve them.
- `.github/AI_POLICY.md` states 'Absolutely no unsupervised agentic tools' and requires a human copyright owner per contribution with no LLM bots in `Co-authored-by:` — do not open PRs against this upstream without explicit human sponsorship.
- Read first, in order: `src/requests/api.py` (surface), `src/requests/sessions.py` (orchestration), `src/requests/models.py` (value objects), `src/requests/adapters.py` (transport). These four contain the whole request lifecycle.
- Local setup and test: `make init` installs `requirements-dev.txt` (editable `.[socks]`, pytest, pytest-httpbin, httpbin, trustme); `make test` runs `pytest tests`; `make ci` matches CI exactly.
- Two additional gates must pass before merge: `pre-commit run --all-files` (ruff-check --fix and ruff-format) and `python -m pyright src/requests/` under `typeCheckingMode = "strict"`.
- Style is enforced by ruff with rule sets E/W/F/I/UP/T10 targeting py310, ignoring E203/E501/UP031, with per-file `E402,F401` exemptions for `src/requests/__init__.py` and `src/requests/compat.py` — do not 'clean up' those unused imports, they are the public re-export surface.
- `src/requests/_types.py` is explicitly documented as not part of the public API; it hosts the `RequestKwargs`/`GetKwargs`/`PostKwargs`/`DataKwargs` TypedDicts consumed via `Unpack` in `api.py` and `sessions.py`, so adding a request kwarg means editing both the runtime signature and the TypedDict.
- `_types.is_prepared()` is a typing-only narrowing helper that returns `True` unconditionally at runtime; the many `assert _is_prepared(request)` lines are type assertions, not validation — do not convert them into runtime checks.
- Safe edit boundary: new transport behaviour belongs in an `HTTPAdapter` subclass or the documented override methods, not in `Session.send`, because `Session.send` is also reached by `resolve_redirects` and any change there multiplies across every redirect hop. (inferred)
- Trap: `prepare_cookies` can only meaningfully run once per PreparedRequest — cookielib will not regenerate the header if `Cookie` already exists, which is why `resolve_redirects` pops the header before re-preparing.
- Trap: branch comments cite issue numbers as the rationale (e.g. #1084 for the 307/308 carve-out, #3490 for the purged headers, #1704 for POST-301→GET, #3772 for digest-only-on-4xx, #5229 for the `fileno` AttributeError). Look these up before changing the branch.
- Trap: `compat.py` rebinds `str` and `bytes` to the builtins and exports `basestring = (str, bytes)`; modules importing from it (`auth.py`, `utils.py`, `adapters.py`) rely on these shadows, so removing them is a wide breaking change.
- Tests requiring network-like behaviour need the local httpbin fixture; use the `httpbin` fixture (which yields a URL-joining callable, not a string) rather than hardcoding `https://httpbin.org`.
Languages
| Language | Files | Share of source |
|---|---|---|
| Python | 37 | 97% |
| Markdown | 13 | docs/config |
| YAML | 12 | docs/config |
| CSS | 1 | docs/config |
| HTML | 1 | docs/config |
| TOML | 1 | 3% |
Start reading here
Probable source entrypoint: setup.py — inferred from the directory tree and manifest, not the upstream docs.
Source roots
src/ (20 files)
Source layout — src/
| Path | Files |
|---|---|
src/requests/ | 20 |
Repository structure
| Top-level path | Files |
|---|---|
tests/ | 42 |
docs/ | 25 |
src/ | 20 |
.github/ | 19 |
ext/ | 2 |
Docs found: README.md
Package
pyproject.toml — requests v{attr = "requests.__version__.__version__"}, 3 declared dependencies.
securitysocksuse_chardet_on_py3
CI & config entrypoints
README.md— Repository readme.github/workflows/publish.yml— GitHub Actions workflow.github/workflows/codeql-analysis.yml— GitHub Actions workflow.github/workflows/lint.yml— GitHub Actions workflow.github/workflows/typecheck.yml— GitHub Actions workflow.github/workflows/run-tests.yml— GitHub Actions workflow.github/workflows/lock-issues.yml— GitHub Actions workflow.github/workflows/zizmor.yml— GitHub Actions workflow.github/workflows/close-issues.yml— GitHub Actions workflow
Structural health signals
Average score 7.7/10 across 22 analyzed source files; 5 hotspots surfaced.
src/requests/models.py— 1184 lines, score 5.4 (Large file, Deep nesting)src/requests/sessions.py— 920 lines, score 5.4 (Large file, Deep nesting)src/requests/utils.py— 1155 lines, score 5.7 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
611c6162cbc42026-09-21 — Bump the actions group with 3 updates (#7628)dae7ef63b4df2026-09-02 — Bump https://github.com/astral-sh/ruff-pre-commit (#7616)5460f467b02e2026-08-24 — Update lock-issues.yml to latest dependencies (#7609)6af0b94158bb2026-08-24 — Bump https://github.com/astral-sh/ruff-pre-commit (#7608)8f8b212de8c22026-08-17 — Bump https://github.com/astral-sh/ruff-pre-commit (#7606)