# requirements.txt

> requirements.txt is pip's requirements file: a list of Python packages to install, plus pip options that can change where and how they install.

Also known as: pip requirements file, requirements file, Python requirements file

Source: https://socket.dev/glossary/requirements-txt

Last updated: 2026-10-30

**requirements.txt**, also called a pip requirements file, is the text file that pip reads with `pip install -r` to install a Python project's dependencies. Each line is a requirement specifier or a pip command-line option, so the file controls versions, package indexes, and hash checks. requirements.txt is a pip convention, not a standard, and it is not secure by default.

The part most guides leave out: a requirements.txt file is a pip command line saved to disk. As of October 2026, pip 26.2.2 accepts [17 global options inside the file](https://github.com/pypa/pip/blob/26.2.2/src/pip/_internal/req/req_file.py), including `--index-url`, `--extra-index-url`, `--trusted-host`, and `--no-require-hashes`. pip also fetches nested `-r` files over HTTP. One added line can change where every package in the file comes from, and an exact pin does not close that gap. An `==` pin names a version, not a file. In our test, `acme-billing==1.4.2` installed an impostor 1.4.2 from a second index in three runs out of three.

Our position: treat requirements.txt as generated output, not hand-written configuration. Generate it with hashes, install it with `--require-hashes` on the command line, keep index settings out of it, and review every diff to it as you would a change to CI configuration. Hashes then guarantee you install the bytes you locked. Hashes cannot tell you whether those bytes were safe to lock.

## What is requirements.txt?

requirements.txt is a list of things for [pip](https://socket.dev/glossary/pip-python-package-index) to install, written in pip's own input syntax. The filename is only a convention: pip reads whatever file you pass to `-r`, and the [requirements file format](https://pip.pypa.io/en/stable/reference/requirements-file-format/) is defined in pip's documentation, not in a packaging standard. Packages usually come from the [Python Package Index](https://socket.dev/glossary/python-package-index-pypi) unless the file or pip's configuration says otherwise. uv and pip-tools read and write the same format, with small differences covered below.

requirements.txt describes an environment, not a project. A library declares the version ranges it supports in `pyproject.toml`, for example `requests>=2.31`, so its users can share compatible versions. An application or container image uses requirements.txt to record the exact set of packages it was tested with, including every [transitive dependency](https://socket.dev/glossary/transitive-dependency). A requirements.txt that lists only top-level names without pins records neither. It tells pip to install whatever the index serves on the day you run it.

Python's packaging community is explicit about the format's limits. [PEP 751](https://socket.dev/blog/python-adopts-standard-lock-file-format-for-reproducible-installs), accepted on March 31, 2025, defines `pylock.toml` as the standard lock file and makes file hashes mandatory. Its author, Brett Cannon, contrasts that with ["requirements files which can optionally include hashes, but it is an opt-in feature and can be bypassed."](https://peps.python.org/pep-0751/) The next two sections show how.

## Every line in requirements.txt is a pip instruction

A requirements.txt line is one of four kinds, and only two of them name a package. Figure 1 annotates a file that uses all four.

> An annotated requirements.txt file. Global options change every package in the run, includes pull in other files including over HTTP, requirement lines name one package, and per-requirement options such as --hash apply to one package.

Figure 1 in text: the four kinds of requirements.txt line, what each looks like, and how far its effect reaches.

| Line kind | Examples | Scope | What it can change |
| --- | --- | --- | --- |
| Global option | --index-url, --extra-index-url, --trusted-host, --find-links, --no-index, --pre, --only-binary, --require-hashes, --no-require-hashes | Every package in the pip run | Where packages come from, whether a host needs valid HTTPS, whether hashes are enforced |
| Include | -r base.txt, -r https://example.com/base.txt, -c constraints.txt | Everything in the included file | Anything the included file says, including global options |
| Requirement | urllib3\>=2,\<3 ; python\_version \>= "3.10", mylib @ git+https://github.com/example/mylib@3f2c1e0, -e ./libs/shared | One package | Acceptable versions, or a direct source that bypasses the index |
| Per-requirement option | --hash=sha256:..., --config-settings | One package | Which file digests are acceptable, and build backend settings |

Three parsing rules complete the format. pip expands POSIX-style `${VAR}` references from the environment at install time, and it does not expand `$VAR` or `%VAR%`. A trailing backslash continues a line, and `#` starts a comment. Direct references to Git repositories behave like the [Git and URL dependencies](https://socket.dev/glossary/git-dependencies) other ecosystems have: they skip the index, and a hash cannot cover them.

pip's parser defines exactly which options a file may use. As of pip 26.2.2, [`req_file.py`](https://github.com/pypa/pip/blob/26.2.2/src/pip/_internal/req/req_file.py) allows 17: `--index-url`, `--extra-index-url`, `--no-index`, `-c`, `-r`, `-e`, `--find-links`, `--no-binary`, `--only-binary`, `--prefer-binary`, `--require-hashes`, `--no-require-hashes`, `--pre`, `--all-releases`, `--only-final`, `--trusted-host`, and `--use-feature`. The index, trust, binary, release, and hash options apply to the whole install, not to the line they sit near.

### A remote include can change your index

Includes are where the command-line nature of requirements.txt gets dangerous. pip fetches an `-r` target over HTTP or HTTPS at install time, so part of your dependency list can live on a server you do not control and can change without a commit. We committed this file:

```text
--index-url http://localhost:8011/
-r http://localhost:8020/extra.txt
acme-billing
```

The included file, served from a different local web server, held one line:

```text
--extra-index-url http://localhost:8012/
```

The private index on port 8011 served acme-billing 1.4.2. The second index on port 8012, standing in for a public registry, served acme-billing 9000.0.0. pip 26.2.2 installed the public package:

```text
$ python -m pip install -r requirements.txt
Collecting acme-billing (from -r requirements.txt (line 3))
  Downloading acme_billing-9000.0.0-py3-none-any.whl (1.2 kB)
Installing collected packages: acme-billing
Successfully installed acme-billing-9000.0.0
```

The committed requirements.txt names only the private index. The remote include added a second one, and pip's rule of pooling every index and taking the highest version did the rest. The result is [dependency confusion](https://socket.dev/glossary/dependency-confusion) delivered through an include. uv 0.13.6 followed the same include and installed the same 9000.0.0. A reviewer reading the repository would see nothing wrong.

The `${VAR}` syntax is how most teams put private index credentials into requirements.txt, as in `--extra-index-url https://${PIP_TOKEN}@pkgs.example.com/simple/`. Expansion keeps the token out of Git. It also makes requirements.txt the place where package sources are configured, which is the line an attacker most wants to edit. We prefer index settings in `pip.conf` or `PIP_INDEX_URL` on the build machine. That does not stop someone who can edit CI configuration. It does mean a pull request that only adds a dependency cannot also change where dependencies come from.

## How do you install requirements.txt?

You install requirements.txt by running `python -m pip install -r requirements.txt` inside an activated [virtual environment](https://socket.dev/glossary/virtual-environment-python). The `python -m` form binds pip to the interpreter you name. The virtual environment keeps the packages out of the system Python, which many Linux distributions now refuse to modify.

```bash
python3 -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
python -m pip install -r requirements.txt
```

uv's equivalent is `uv pip install -r requirements.txt`. uv reads the same format, but not every option: uv 0.13.6 stopped with a parse error on a `--no-require-hashes` line in our test.

For CI and container builds, we install a generated, fully hashed requirements.txt with a stricter command. Each flag sits on the command line on purpose, where the file cannot override it:

```bash
python -m pip install \
  --require-hashes \
  --only-binary :all: \
  --no-deps \
  -r requirements.txt
```

`--require-hashes` belongs on the command line because requirements.txt can now switch off the implicit form of hash checking. Since pip 26.2, released July 29, 2026, pip's documented rule that one hashed requirement makes every requirement need a hash can be disabled with [`--no-require-hashes`](https://pip.pypa.io/en/stable/news/). That option is also legal inside the file. We confirmed it. A file with one hashed and one unhashed requirement failed by default:

```text
ERROR: In --require-hashes mode, all requirements must have their versions pinned with ==. These do not:
    acme-tools from http://localhost:8011/acme-tools/acme_tools-2.0.0-py3-none-any.whl#sha256=ec47... (from -r req-mixed.txt (line 4))
```

Appending `--no-require-hashes` to the same file made both packages install, the unhashed one unchecked. Passing `--require-hashes` explicitly made pip refuse the combination instead:

```text
$ python -m pip install --require-hashes -r req-mixed-off.txt
ERROR: --require-hashes and --no-require-hashes are mutually exclusive
```

pip added `--no-require-hashes` for a legitimate case: mixing hashed requirements with local directories or VCS URLs that cannot be hashed. Our point is narrower. Never rely on implicit hash mode in an install you care about.

The other two flags close smaller gaps. `--only-binary :all:` refuses source distributions, so no `setup.py` runs while pip resolves. `--no-deps` makes pip install exactly the listed set, which a complete lock already describes. pip's [secure installs guide](https://pip.pypa.io/en/stable/topics/secure-installs/) opens with the reason all of this is opt-in: "By default, pip does not perform any checks to protect against remote tampering."

A [dependency cooldown](https://socket.dev/glossary/dependency-cooldown) cannot live in requirements.txt at all. pip's `--uploaded-prior-to` filter is not one of the 17 allowed options:

```text
$ cat requirements.txt
--uploaded-prior-to P3D
requests
$ python -m pip install -r requirements.txt
ERROR: Invalid requirement: --uploaded-prior-to P3D
__main__.py: error: no such option: --uploaded-prior-to
```

Set the cooldown in `pip.conf` or `PIP_UPLOADED_PRIOR_TO` for unpinned installs. For a pinned file, apply it when you generate the file, which the next section covers.

## How do you generate requirements.txt in Python?

You generate requirements.txt by resolving a short list of direct dependencies into a pinned, hashed file. The common tools are [`pip-compile --generate-hashes`](https://pip-tools.readthedocs.io/en/stable/reference/pip-compile/) from pip-tools, `uv pip compile --generate-hashes`, and `uv export --format requirements.txt` inside a uv project. `pip freeze > requirements.txt` also produces a file, but pip freeze takes a snapshot of an environment rather than resolving your dependencies.

The difference shows on a single package. We started from a `requirements.in` containing one line, `requests`:

```text
$ pip-compile --generate-hashes --strip-extras -o requirements.txt requirements.in
$ grep -c -- '--hash' requirements.txt
180
$ grep -A4 '^requests' requirements.txt
requests==2.34.2 \
    --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \
    --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed
    # via -r requirements.in
```

One input line became five pinned packages and 180 hashes. charset-normalizer alone accounted for 172 of them, one for each platform-specific wheel plus the source distribution. `uv pip compile --generate-hashes` produced the same five pins and the same 180 hashes. A hashed requirements.txt is unreadable by hand by design, and the only safe way to change it is to regenerate it.

pip freeze records something else:

```text
$ python -m pip install requests
$ python -m pip uninstall -y requests
$ python -m pip freeze
certifi==2026.10.23
charset-normalizer==3.5.2
idna==3.20
urllib3==2.8.0
```

pip freeze lists everything installed, including the four orphans left behind after we removed requests. pip freeze records no hashes and no sources. In the environment from the remote-include demo, `pip freeze` printed `acme-billing==9000.0.0`, with nothing to say the package came from the wrong index.

A cooldown belongs at this step. `pip-compile --pip-args "--uploaded-prior-to P10D"` resolved certifi 2026.10.19 instead of the 2026.10.23 release from a week earlier, and `uv pip compile --exclude-newer "10 days"` did the same. Malware that is reported and removed inside the window never reaches a pin locked that way, and the window is usually long enough: [PyPI reported in 2024](https://socket.dev/blog/pypi-slashes-malware-response-time) that since August 2023 it had resolved over 90% of malware reports in under 24 hours.

| Method | Pins transitive dependencies | Hashes | Records why each package is present | Verdict |
| --- | --- | --- | --- | --- |
| Hand-written names | No | No | No | Use only as input, in requirements.in |
| pip freeze | Whatever happens to be installed | No | No | Debugging snapshots only |
| pip-compile --generate-hashes | Yes | Yes, every file | Yes, # via comments | Default for pip-based applications |
| uv pip compile --generate-hashes | Yes | Yes, every file | Yes, # via comments | Same result, faster resolver |
| uv export --format requirements.txt | Yes, from uv.lock | Yes, by default | Yes, # via comments | When uv.lock is the real lock and another tool needs requirements.txt |

Our verdict: write direct dependencies in `requirements.in`, generate requirements.txt with a resolver that emits hashes, and commit both. Use pip freeze to inspect an environment, never to define one.

## Does pinning with == make requirements.txt safe?

No: an `==` pin in requirements.txt fixes a version number, but pip accepts any file with that version from any configured index. Figure 2 shows the experiment.

> Experiment with two package indexes that both serve acme-billing 1.4.2 with different contents. With an == pin, both files match and pip installed the public impostor. With a --hash pin and an index that publishes hashes, pip installed the private file. With a --hash pin and an index that does not publish hashes, pip downloaded the impostor and failed with a hash mismatch.

Figure 2 in text: two local indexes both served acme-billing 1.4.2. The private copy had sha256 `e10dd02fc96068e5fb7e34a21c1ee83cf3900eedda759eb828920a1f6c3597a6`. The public stand-in had sha256 `18171155c059b8289a3efeae396b3f7d4ab360a211c77f0345ca1d15a2d56d52` and also served 9000.0.0. The results for three versions of the same requirement:

| Requirement line | Files that match | Result with pip 26.2.2 |
| --- | --- | --- |
| acme-billing==1.4.2 | Private 1.4.2 and public 1.4.2 | Public 1.4.2 installed, 3 of 3 runs |
| acme-billing==1.4.2 --hash=sha256:e10dd02f... (index publishes hashes) | Private 1.4.2 only | Private 1.4.2 installed |
| Same line, index publishes no hashes | Unknown until download | Public file downloaded, hash mismatch, install fails |

The unhashed pin used this file:

```text
--index-url http://localhost:8011/
--extra-index-url http://localhost:8012/
acme-billing==1.4.2
```

`pip install --dry-run --report` confirmed that the chosen download URL came from the second index. Swapping the two index lines flipped the result, so in pip 26.2.2 a tie between identical versions went to the index listed last. pip's documentation promises no order: ["There is no priority in the locations that are searched."](https://pip.pypa.io/en/stable/cli/pip_install/) Treat the tie-break as an implementation detail. uv 0.13.6 also installed the public copy, because uv searches `--extra-index-url` entries before `--index-url`.

Adding the private file's hash changed the outcome. With the hash present, pip discarded the public 1.4.2 and installed the private one. pip can do this when the index advertises each file's digest in its links, as PyPI's Simple API does. We then served the same two directories as plain listings with no digests, on ports 8001 and 8002, and kept the hashed line. pip downloaded the public file first and then stopped:

```text
ERROR: THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE. If you have updated the package versions, please update the hashes. Otherwise, examine the package contents carefully; someone may have tampered with them.
    acme-billing==1.4.2 from http://localhost:8002/acme-billing/acme_billing-1.4.2-py3-none-any.whl (from -r requirements.txt (line 3)):
        Expected sha256 e10dd02fc96068e5fb7e34a21c1ee83cf3900eedda759eb828920a1f6c3597a6
             Got        18171155c059b8289a3efeae396b3f7d4ab360a211c77f0345ca1d15a2d56d52
```

Failing closed is the correct result. A broken build is recoverable, and an impostor in production may not be.

The two-index form of this attack has already happened. Between December 25 and December 30, 2022, Linux users who installed PyTorch nightly builds with pip received a malicious `torchtriton` from PyPI instead of from PyTorch's own index. [PyTorch's disclosure](https://pytorch.org/blog/compromised-nightly-dependency/) said that pip "will install their version by default." The malicious binary ran when `triton` was imported and uploaded SSH keys and the first 1,000 files in the user's home directory over DNS.

## What a hashed requirements.txt does not cover

Hashes in requirements.txt prove integrity, not trust. A hash proves that you received the bytes you locked. If you lock a malicious release, hash-checking mode reinstalls it faithfully on every build.

[The LiteLLM compromise](https://socket.dev/blog/litellm-compromise) shows how small the window can be. LiteLLM 1.107.0 reached PyPI at 11:39:51 UTC on October 21, 2026, as seven wheels carrying a `cook.pth` startup hook and a credential collector. Socket's scanner flagged the release at 11:46:30 UTC. Any requirements.txt regenerated while 1.107.0 was available, from an open-ended line such as `litellm` or `litellm>=1.100`, would have locked it, and its hashes would have made the malware reproducible. A cooldown at generation time narrows this gap, provided the malicious release is caught before the cooldown expires.

The second gap is everything the file does not list. After [the Telnyx Python SDK compromise](https://socket.dev/blog/telnyx-python-sdk-compromised) in March 2026, our first recommendation was to audit requirements.txt files for `telnyx==4.87.1` or `telnyx==4.87.2`. That check only answers for installs that used the file. A `RUN pip install telnyx` line in a Dockerfile, a notebook cell, or a CI step that installs a tool directly never touches requirements.txt. We made the same argument about lockfiles after [the axios compromise](https://socket.dev/blog/hidden-blast-radius-of-the-axios-compromise). A [lockfile](https://socket.dev/glossary/lockfile) makes existing installs deterministic, not new installs safe, and a clean one checked after the fact is weak evidence that you were never exposed.

The control that covers both gaps runs before the download. [Socket Firewall Free](https://docs.socket.dev/docs/socket-firewall-free) wraps the install command, as in `sfw pip install -r requirements.txt`, and runs pip behind a local proxy that checks each requested artifact against Socket's malware data before pip receives it. It has documented limits. sfw cannot see packages pip serves from its local cache, so the documentation says to run `pip cache purge` first. The free version blocks only malware confirmed by human review, warns on AI-detected malware, and does not block versions Socket has not yet scanned.

## requirements.txt vs pyproject.toml vs pylock.toml

requirements.txt competes with two standard formats, and each answers a different question. The table compares them as of October 2026.

| Format | What it records | Hashes | Standard | Can redirect package sources | Installer support |
| --- | --- | --- | --- | --- | --- |
| pyproject.toml \[project.dependencies\] | Version ranges a project supports | No | Yes, PEP 621 | Not in \[project\]; tool tables such as \[tool.uv.index\] can | Every modern build tool and installer |
| requirements.txt | Packages and pip options for one install | Optional, and the file itself can relax enforcement | No, pip convention | Yes, from the file and from any include | pip, uv, pip-tools, most scanners |
| pylock.toml | Exact files to install, with their sources | Required | Yes, PEP 751 | No, each package records its own source | pip 26.1+ reads it with -r, still marked experimental; uv and PDM export it |
| uv.lock, poetry.lock | Tool-specific lock of a resolved graph | Yes | No | No, sources are recorded per package | The tool that wrote it |

Our verdict: libraries declare ranges in `pyproject.toml` and ship no requirements.txt. Applications installed with pip should use a generated, hashed requirements.txt today and move to `pylock.toml` when pip drops the experimental label. Applications managed by uv or Poetry should treat that tool's lock file as the source of truth and export requirements.txt only for tools that need it. Nobody should hand-edit a hashed requirements.txt.

## The open problem: a dependency list that can rewrite its own sources

requirements.txt survives because it is simple and every Python tool can read it. Its weakness is the same flexibility. The file that lists packages can also redefine where packages come from, exempt a host from HTTPS with `--trusted-host`, pull in more instructions over HTTP, and, since pip 26.2, switch off the hash enforcement its own hashes turned on. `pylock.toml` removes this class of problem by design, because a lock file that records each file's URL and digest has no options to override.

We think pip should, at minimum, refuse index, trust, and hash-mode options from requirements files included over HTTP, where nobody reviewed them. Until it does, the safest requirements.txt contains nothing but pinned, hashed requirements, is written by a tool, and is installed with the strict flags on the command line.
