Python requirements.txt: What It Is and How to Use It

A requirements.txt file lists the packages a project needs so a fresh install gets the same environment. Generating it from your project instead of the whole machine keeps the list to just the packages you use.

What pip actually does with the file

A requirements file is a plain text file whose lines pip parses as requirement specifiers, the same grammar you would type after pip install on the command line. Nothing in the format is special to the filename. You could call it deps.txt and pass it to the same flag.

Here is a file with three entries.

requests
rich
python-dotenv

pip reads each line as a name, asks the index for the newest release that satisfies every constraint it has collected, and then repeats that resolution for each package’s own dependencies. A file with those entries pulls ten distributions into a virtual environment, because requests declares urllib3, idna, certifi, and charset-normalizer, while rich declares markdown-it-py, pygments, and mdurl.

The resolution step is where a requirements file stops matching your code. The file lists what your code imports directly. The installed set includes those packages plus everything they need, and pip decides the versions for the transitive half on its own unless you pin them.

The line shapes a requirements file accepts

Six line shapes cover nearly every file you will read in the wild. Each one changes what pip is allowed to install, so check which shape you are looking at before you edit it.

LineWhat pip does
richNewest compatible release
requests>=2.32Newest release at or above the floor
urllib3>=1.26,<3Newest release inside both bounds
flask~=3.1Newest 3.x release, refuses 4.0
requests[socks]Adds the package’s optional extra group
A line with an environment markerInstalled only where the marker passes

A bare name takes the newest compatible release. A comparison operator narrows the range, and the comma joins several bounds into one requirement.

rich
requests>=2.32
urllib3>=1.26,<3
flask~=3.1

The tilde equal operator means compatible release, so flask~=3.1 accepts 3.1, 3.2, and 3.9 while refusing 4.0. A wildcard holds the micro version instead, so requests==2.34.* takes 2.34.2 and never jumps to 2.35.

Square brackets select an extra, an optional group of dependencies the package declares in its own metadata. Environment markers after a semicolon decide whether the line applies at all, and pip evaluates them against the interpreter that runs the install.

requests[socks]
tomli; python_version < "3.11"
importlib-metadata; python_version < "3.10"

Markers are evaluated against the interpreter that runs pip rather than the one that wrote the file, which is how one file serves readers across several Python releases. A line guarded with python_version < “3.11” is skipped entirely on Python 3.14.

Two flags pull other files into the same resolve. The include flag merges another requirements file, and the constraint flag applies version limits without adding packages of its own.

-r base.txt
-c constraints.txt

Comments start with a hash and a trailing backslash continues a line, and index or find-links options are legal inside the file as well. That combination is how a project points at a private registry without every contributor exporting an environment variable.

# internal mirror first, then PyPI
--extra-index-url https://pypi.org/simple
--find-links ./vendor/wheels
company-auth==2.4.1

Direct URLs and local paths are valid lines too. They bypass the index, which is what you want for a fork or an in-house package that never reached PyPI.

urllib3 @ https://github.com/urllib3/urllib3/archive/refs/tags/1.26.20.zip
-e ./packages/company-auth

Build a file for a project you already have

The best starting point is a project that already runs on your machine, because that is the state in which you can tell whether the file you generate is complete. I used a small command line weather tool that imports requests, rich, and python-dotenv.

Create a virtual environment on current Python, install nothing else, then run the program. The import fails with the package name in the traceback, and that name is the first line of your requirements file.

python -m venv .venv
source .venv/bin/activate
python weathercli/weather_cli.py
Terminal showing ModuleNotFoundError: No module named requests when the weather script runs in a fresh virtual environment
Running the tool before its dependencies exist

Install those imports and the same file runs.

python -m pip install requests rich python-dotenv
python weathercli/weather_cli.py

Now ask the environment what it contains. The freeze output is a valid requirements file, and it is also the reason so many of them rot, because it records all ten distributions rather than the three you named.

python -m pip freeze > requirements.txt
Terminal output of python -m pip freeze listing ten pinned packages, including certifi, charset-normalizer, idna, markdown-it-py, mdurl, Pygments, python-dotenv, requests, rich and urllib3
Ten pinned packages from three direct imports

On a single project machine that file is fine and it installs fast. The trouble arrives when the environment also holds a linter you installed once for another task, because freeze cannot tell the difference between a dependency of your program and a package you happened to install in the same environment.

  • Run the program and read the first failing import, because that name belongs in the file.
  • Install the imports you found, then run the program again to confirm the environment is complete.
  • Record the versions with a generation command rather than by hand.

Generate the list from imports instead of from the environment

Tools that read your source files avoid the pollution problem by scanning import statements rather than the site-packages directory. That is a different question from freeze, and it gives a shorter file that matches your code.

pipreqs answers the same question by scanning import statements instead of the installed environment. On Python 3.14 it installs an old release, because version 0.5.0 declares a Python ceiling that excludes 3.13 and later, and pip resolves to 0.4.13 from April 2023 instead of failing.

python -m pip install "pipreqs==0.5.0"
Terminal showing pip refusing pipreqs 0.5.0 because it requires a Python version below 3.13
pip will not install the newer pipreqs on Python 3.14

Running the older release still produces a usable file. It writes the project’s imports with pinned versions, and it prints a warning for each name it had to resolve against the index, including a capitalised Requests that comes from its own lookup rather than from PyPI.

pipreqs weathercli

uv answers the same question with a resolver that records why each entry exists, and it compiles a short input file into a pinned output file. Every line in the result carries a comment naming the package that pulled it in.

uv pip compile requirements.in -o requirements-lock.txt
Terminal output of uv pip compile showing ten pinned packages, each annotated with a via comment naming its parent dependency
Compiled output keeps the provenance of every pinned line

The via comments make a pinned file maintainable. When a pin breaks a build down the line, the comment tells you whether you can raise it yourself or whether a library you depend on set that ceiling.

Install from the file on another machine

Create the environment first, then point pip at the file with the module form so the pip you call belongs to the interpreter you just created.

python -m venv verify-venv
source verify-venv/bin/activate
python -m pip install -r requirements-lock.txt

I ran that sequence against the compiled file and then executed the weather tool inside the new environment, and the printed table matched the original. The table matching is the test that matters, because the file is complete when a fresh environment reproduces your program’s behavior.

When the file lives somewhere else you pass its path to the same flag, and a URL works as well. A URL also lets a CI job install straight from a tag without a checkout.

python -m pip install -r ci/requirements-lock.txt
python -m pip install -r https://example.com/locks/requirements-lock.txt

Once the pins exist, you can verify the artifacts as well as the names. Generate hashes into the file and pip will refuse any download whose digest does not match, which turns a tampered or substituted wheel into a hard error.

uv pip compile requirements.in --generate-hashes -o requirements-hashed.txt
python -m pip install --require-hashes -r requirements-hashed.txt

The verbose flag is the fastest way to find a broken mirror. Running pip install -v -r requirements.txt prints the artifact URL pip intends to fetch for every package, so a stale index shows up in the first few lines instead of after an install that appears to be doing nothing.

Split runtime and development requirements

A production image does not need pytest, and a developer machine should not install the whole test stack into the runtime environment by accident. Splitting the runtime list from the tooling list keeps that boundary clear.

Keep the runtime names in base.txt, then have dev.txt include base and add the tooling on top.

# requirements/base.txt
requests>=2.32
rich
# requirements/dev.txt
-r base.txt
pytest

An include carries its own lines but not its own version limits, so a second file can hold the ceilings you want applied to the whole resolve. That file adds no packages, and that is the point.

# requirements/constraints.txt
urllib3<2

The constraint changes which release wins. Installing the development file without it selects urllib3 2.7.0, and adding the constraint drags the resolved version back to 1.26.20 even though requests would have accepted either.

python -m pip install -r requirements/dev.txt -c requirements/constraints.txt
Terminal showing urllib3 pinned to 1.26.20 after installing the development requirements with a constraints file
The constraints file overrides the newest acceptable release

Use constraints when a downstream service is known to reject a newer library, and prefer them over editing every requirements file. One line in constraints.txt applies to every resolve that includes it.

Lock versions for a repeatable install

A pinned requirements file answers what the project needs. A lock file answers what was actually installed, down to the wheel and its checksum, and the format for that has a standard now.

pip ships a lock command that writes pylock.toml, the newer project file format for locked resolutions. It records each package with the exact wheel URL and the sha256 of the artifact.

python -m pip lock -r requirements-lock.txt -o pylock.toml
lock-version = "1.0"
created-by = "pip"

[[packages]]
name = "certifi"
version = "2026.7.22"

[[packages.wheels]]
name = "certifi-2026.7.22-py3-none-any.whl"
url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl"

[packages.wheels.hashes]
sha256 = "62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"

The command is marked experimental in pip 26.2.1, so treat the output as a working artifact rather than a frozen contract. What it does record is complete: every wheel URL plus the sha256 digest pip will accept for that artifact.

Two other tools cover the same ground with more settled file formats. pip-tools writes a pinned requirements file from an input file, and uv compiles one that installs noticeably faster because it resolves before touching the environment.

pip-compile requirements.in -o requirements-piptools.txt
uv pip compile requirements.in -o requirements-lock.txt --upgrade

Pick one tool per repository and commit its output, because two lock sources means two resolutions for the same environment. Which one wins then depends on the order of your install steps.

The errors you will hit, and what each one means

Three failure messages cover almost every broken requirements file. Each names the package it could not satisfy, so the fix starts with reading the last line rather than the first.

When a hard version pin does not exist, pip prints the full list of releases it considered along with why each one was skipped, which is why the message runs long. The first line is the one to read, because it says whether the version is missing, yanked, or excluded by the Python requirement.

python -m pip install "requests==99.99.99"
ERROR: Ignored the following yanked versions: 2.32.0, 2.32.1
ERROR: Could not find a version that satisfies the requirement requests==99.99.99
ERROR: No matching distribution found for requests==99.99.99

Two requirements that cannot both be true produce a different message. pip calls this ResolutionImpossible and prints the two constraints that collided before it suggests loosening a range.

python -m pip install "requests>=2.32" "urllib3<1"
ERROR: Cannot install urllib3<1 and requests>=2.32 because these package versions have conflicting dependencies.
ERROR: ResolutionImpossible: for help visit https://pip.pypa.io/en/latest/topics/dependency-resolution/

The hardest failure to notice is an extra that no longer carries anything. requests 2.34.2 still declares a security extra in its metadata, but that extra has no requirements attached, so installing requests[security] pulls requests and its base dependencies and nothing else.

python -m pip install "requests[security]"
python -c "from importlib.metadata import metadata; print(metadata('requests').get_all('Provides-Extra'))"

Check what an extra resolves to before you rely on it, because the name can stay in the metadata after the dependencies behind it have gone.

What is the difference between requirements.txt and a lock file?

requirements.txt lists the packages your project needs, and pip is free to choose versions that satisfy the ranges you wrote. A lock file records the exact versions, wheel URLs, and hashes from one resolution, so two installs from the lock file produce the same result.

Should I pin every version in requirements.txt?

Pin the direct dependencies with a compatible-release operator when you want security updates inside a known-good series, and pin transitives through a lock file or constraints file. An unpinned file can drift between installs, and a fully pinned file hides which ceiling belongs to which library.

How do I install a requirements.txt file that is inside a folder?

Pass the path directly to the requirement flag, as in python -m pip install -r path/to/requirements.txt. Activate the virtual environment first so pip installs into that environment rather than the system interpreter.

Does pipreqs still work on current Python versions?

pipreqs 0.5.0 declares a Python ceiling of below 3.13, so on Python 3.14 pip resolves to pipreqs 0.4.13 instead and prints no warning about the substitution. Scanning the install with the module form of pip tells you which release you actually got.

Ashutosh Yadav
Ashutosh Yadav
Articles: 30