Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

pip install -r requirements.txt: What It Does and What It Cannot Do

Sean

Platform Writer

Aug 10, 2026
8 min read

pip install -r requirements.txt reads the file line by line and installs each package listed. It is not a lockfile, it does not guarantee the same result twice, and it does not record why a package is there. Understanding those three limits is what separates a project that installs cleanly in two years from one that does not.

pip install -r requirements.txt: What It Does and What It Cannot Do

The command is trivial. What is not trivial is that most requirements.txt files are doing a job they were never designed for — acting as a lockfile — and the failure shows up months later when a fresh install produces a different tree from the one running in production.

Table of contents

The basics, done correctly

Always install into a virtual environment. Installing into the system Python is how you end up with a broken package manager on Linux, which is why newer Python versions refuse outright with an externally-managed-environment error.

python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate

pip install --upgrade pip
pip install -r requirements.txt

Useful variations:

# Multiple files
pip install -r requirements.txt -r requirements-dev.txt

# Upgrade everything to the newest allowed by the constraints
pip install -r requirements.txt --upgrade

# Fail fast rather than compiling from source
pip install -r requirements.txt --only-binary :all:

# See what would be installed without doing it
pip install -r requirements.txt --dry-run

--dry-run is underused and free. It resolves the full tree and prints it, which catches conflicts before you have half-installed anything.

What the file can contain

Requirements files support considerably more than a list of names, and most of it is useful.

# Exact pin -- reproducible
django==5.0.6

# Compatible release: >=2.31.0, <3.0.0
requests~=2.31.0

# Range
sqlalchemy>=2.0,<2.1

# Environment markers -- only on matching platforms
pywin32==306; sys_platform == 'win32'
uvloop==0.19.0; sys_platform != 'win32'
backports.zoneinfo==0.2.1; python_version < '3.9'

# Optional extras
celery[redis]==5.3.6

# Direct from git, pinned to a commit rather than a branch
git+https://github.com/org/pkg.git@a1b2c3d#egg=pkg

# Include another file
-r requirements-base.txt

# Editable local install
-e .

Environment markers are the fix for the requirements.txt that only works on one operating system. A Windows-only dependency listed unconditionally breaks every Linux build, and a one-line marker solves it permanently.

Git dependencies must be pinned to a commit SHA. @main means the install is whatever that branch contained at install time, which is the opposite of reproducible and a supply-chain risk besides.

Why pip freeze is not a lockfile

The usual workflow is pip install django, then pip freeze > requirements.txt. It produces a file that installs correctly today and has three real problems.

  • No distinction between direct and transitive dependencies. You get sixty pinned packages with no indication which four you actually asked for. Removing a package leaves its dependencies pinned forever.
  • No hashes. Nothing verifies the package you download is the one that was pinned.
  • Environment contamination. It freezes whatever is installed, including things you added while experimenting.

The layered approach fixes the first problem with no new tools: state intent in one file, generate the pins into another.

# requirements.in -- what you actually want, loosely pinned
django>=5.0,<5.1
psycopg[binary]>=3.1
celery[redis]>=5.3
gunicorn>=21.2
pip install pip-tools

# Generate a fully-pinned, annotated requirements.txt
pip-compile requirements.in

# Sync the environment to match exactly -- removes extras too
pip-sync requirements.txt

# Upgrade one package deliberately
pip-compile --upgrade-package django requirements.in

The generated file annotates every line with which requirement pulled it in, which turns why is this package here from an investigation into a comment.

pip-sync is the part people miss. pip install -r adds packages but never removes ones you have uninstalled from the file, so environments drift. pip-sync makes the environment match the file exactly.

Hash verification, for anything that matters

Version pinning says this version. Hash pinning says these exact bytes. The gap between them is a compromised package index or a maliciously re-uploaded release.

pip-compile --generate-hashes requirements.in
django==5.0.6 \
    --hash=sha256:8e0f1c2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6 \
    --hash=sha256:9f1e2d3c4b5a69788796a5b4c3d2e1f0918273645546372819a0b1c2d3e4f5a6
    # via -r requirements.in

With hashes present, pip refuses to install anything that does not match — and it requires every package to be hashed, which prevents a partial rollout that quietly protects nothing.

The cost is friction on upgrades, since every change means regenerating. For an application handling real data, that friction is the point. For a scratch project it is overhead.

Layering dev and production

Production should not install pytest. A single file forces you to choose between shipping test tooling or not having it locally.

# requirements.txt  -- production
django==5.0.6
gunicorn==21.2.0
psycopg[binary]==3.1.18

# requirements-dev.txt -- everything above, plus tooling
-r requirements.txt
pytest==8.2.0
pytest-cov==5.0.0
ruff==0.4.4
mypy==1.10.0

The -r requirements.txt line means the dev file always includes production, so they cannot drift apart.

In Docker this layering also buys you cache efficiency — copy the requirements file and install before copying application code, so a source change does not reinstall every dependency:

FROM python:3.12-slim

WORKDIR /app

# This layer is cached until requirements.txt changes
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Source changes invalidate only from here down
COPY . .

CMD ["gunicorn", "app.wsgi:application", "--bind", "0.0.0.0:8000"]

--no-cache-dir in a container image is worth the two seconds. The pip cache is dead weight in a layer that never reuses it.

Should you still be using requirements.txt at all?

Honestly: for a new project, probably not. The ecosystem has moved to pyproject.toml as the standard place to declare dependencies, and the tooling around it is better.

  • pyproject.toml with uv — very fast resolution, a real lockfile, and Python version management in one tool. The current default recommendation for new projects.
  • Poetry — mature, opinionated, handles packaging and publishing alongside dependencies.
  • PDM — standards-focused, closely tracks PEP developments.
  • pip-tools — the smallest step up from requirements.txt, and the right choice when you cannot change the workflow wholesale.

That said, requirements.txt is not going away, because every deployment target understands it and it has no dependencies of its own. Most modern tools can export to it for exactly that reason.

uv export --format requirements-txt > requirements.txt
poetry export -f requirements.txt --output requirements.txt

That is the pragmatic position: manage dependencies with a modern tool, export a pinned requirements.txt for the build. On RunxBuild, Python services install from the requirements file at build time with the full log visible, so a resolution failure shows the actual pip output rather than a generic error — the Python services documentation covers the build stage.

How this fits the rest of the stack

The command is one line; the reproducibility is the work. Split intent from pins, generate the pinned file rather than freezing your environment, layer dev separately from production, and add hashes for anything handling real data. If you are sizing a Python service and want the build and runtime costs as separate numbers, the RunxBuild hosting calculator shows them apart.

Useful related references:

FAQ

What does pip install -r requirements.txt do?

It reads the file line by line and installs each package specification listed, resolving dependencies as it goes. It is not a lockfile — the same file can produce different trees at different times unless every package is exactly pinned.

What is the difference between pip freeze and pip-compile?

pip freeze dumps whatever is currently installed, with no distinction between packages you asked for and their dependencies. pip-compile reads a requirements.in of your actual intent and generates a fully pinned, annotated file showing why each package is present.

How do I make requirements.txt truly reproducible?

Pin every package exactly, generate the file with pip-compile rather than pip freeze, and add —generate-hashes so pip verifies the downloaded bytes. Use pip-sync instead of pip install -r so removed packages actually leave the environment.

How do I separate development and production dependencies?

Keep production packages in requirements.txt and put tooling in requirements-dev.txt, starting that file with -r requirements.txt so it always includes production and the two cannot drift apart.

Should I use requirements.txt or pyproject.toml?

For new projects, pyproject.toml with uv or Poetry gives real lockfiles and faster resolution. requirements.txt remains useful as an export format because every deployment target understands it.

#pip install requirements.txt#python dependencies#virtual environment#pip-tools#reproducible builds