Published on

pip freeze Is Not a Lockfile: Reproducible Python Requirements

Authors

pip freeze > requirements.txt is useful, but it does not turn an arbitrary Python environment into a portable lockfile. It records the distributions installed in one environment. That distinction matters when the file is recreated on another Python version, operating system, CPU architecture, or package index.

A dependable workflow answers three separate questions:

  1. Which Python interpreter is receiving the install?
  2. Which dependencies does the project intend to use?
  3. Can a clean environment install and run from the recorded inputs?

This guide builds that workflow with the standard venv module and pip. The commands were checked against the current Python and pip documentation on August 13, 2026; pip's published documentation was version 26.2.1 at that check.

TL;DR

  • Prefer python -m pip or an explicit virtual-environment interpreter over a bare pip command.
  • Create a disposable .venv; do not commit or copy the environment directory.
  • Treat pip freeze as a snapshot of everything installed, not as a declaration of direct dependencies or a solver-generated lock.
  • Rebuild from requirements.txt in a new environment and run pip check plus the project's tests.
  • Regenerate snapshots intentionally when Python, the target platform, or top-level requirements change.
  • Add hashes or a wheelhouse when artifact identity and offline installation matter.

Run pip through the intended interpreter

A bare pip executable is resolved through PATH. It can belong to a different Python installation than the python command used to run the application. The module form removes that ambiguity:

python -m pip --version
python -c "import sys; print(sys.executable)"

The two paths should describe the same interpreter environment. On systems where the launcher names are versioned, be explicit when creating the environment:

python3 -m venv .venv

On Windows, the Python launcher can select a version before creating the environment:

py -3 -m venv .venv

Activation is convenient for interactive work, but it is not required. Scripts and CI jobs can invoke the environment's interpreter directly:

.venv/bin/python -m pip --version
.venv/bin/python -c "import sys; print(sys.executable)"

Windows PowerShell:

.\.venv\Scripts\python.exe -m pip --version
.\.venv\Scripts\python.exe -c "import sys; print(sys.executable)"

This pattern prevents a common failure: creating one virtual environment, then installing into a global or unrelated environment because another pip appeared first on PATH.

Keep the virtual environment disposable

Python's venv documentation describes virtual environments as isolated, disposable, and not movable or copyable. Commit the inputs needed to rebuild .venv, not .venv itself.

A minimal .gitignore entry is:

.venv/

Recreating an environment should be routine:

rm -rf .venv
python3 -m venv .venv
.venv/bin/python -m pip install --upgrade pip

On Windows PowerShell:

Remove-Item -Recurse -Force .venv
py -3 -m venv .venv
.\.venv\Scripts\python.exe -m pip install --upgrade pip

Do not solve an EXTERNALLY-MANAGED error by routinely adding --break-system-packages. That marker exists so the operating system's package manager and pip do not overwrite each other's files. Create a virtual environment unless there is a deliberate system-management reason not to.

Understand what pip freeze records

The official command description is precise: pip freeze outputs installed packages in requirements format. It reports the current environment; it does not compute a lockfile or a fresh dependency solution.

python -m pip freeze > requirements.txt

The output can contain:

  • packages the application imports directly;
  • transitive dependencies selected by pip;
  • development and test tools installed in the same environment;
  • unrelated packages left from earlier experiments;
  • editable or direct-URL installs;
  • platform-specific dependencies.

It can also omit bootstrap packaging tools by default. The exact omission differs by Python generation; current pip documentation says Python 3.12 and later omit only pip by default, while earlier versions omit additional bootstrap tools. --all includes them when a complete inventory is actually required.

This means the following sequence is risky:

# A long-lived environment already contains unrelated tools.
python -m pip install httpx
python -m pip freeze > requirements.txt

The generated file answers “what is installed here now?” It does not answer “which packages does this project directly depend on?”

Separate project intent from an environment snapshot

For an installable library, declare runtime dependencies in the project's [project] table in pyproject.toml:

[project]
name = "example-client"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
  "httpx>=0.27,<1",
]

[project.optional-dependencies]
test = [
  "pytest>=8,<9",
]

Those are abstract project requirements: they tell installers what the distribution needs without pinning every transitive package for every consumer.

An application deployment has a different goal. It may use a fully pinned requirements file as the concrete environment input:

anyio==4.10.0
certifi==2025.8.3
httpcore==1.0.9
httpx==0.28.1
idna==3.10
sniffio==1.3.1

The versions above are illustrative, not a recommendation to copy them indefinitely. Resolve them for the application's supported Python and target platforms, then test that exact result.

A practical repository can keep both layers:

pyproject.toml             # direct project intent
requirements.txt           # pinned runtime snapshot
requirements-dev.txt       # pinned development and test snapshot

Keeping production and development tools separate prevents a formatter or test runner from entering the production image merely because it happened to be installed when pip freeze ran.

Build the snapshot from a clean environment

Start from declared top-level requirements rather than an old workstation environment:

rm -rf .venv
python3 -m venv .venv
.venv/bin/python -m pip install --upgrade pip
.venv/bin/python -m pip install .
.venv/bin/python -m pip freeze > requirements.txt

If the project is not packaged, install a small reviewed input file first:

# requirements.in
httpx>=0.27,<1
.venv/bin/python -m pip install -r requirements.in
.venv/bin/python -m pip freeze > requirements.txt

Review the diff. A transitive upgrade may be valid, but it should not arrive unnoticed. Record the Python version and target platform used to generate the snapshot in project documentation or CI configuration.

Verify the file in a second environment

Successful generation proves only that the first environment contained those packages. The useful test is a clean install:

python3 -m venv .verify-venv
.verify-venv/bin/python -m pip install --upgrade pip
.verify-venv/bin/python -m pip install -r requirements.txt
.verify-venv/bin/python -m pip check
.verify-venv/bin/python -m pytest

pip check verifies that installed packages have compatible declared dependencies. It does not run the application or prove behavioral compatibility, so keep unit, integration, import, and startup tests after it.

For a service, a stronger CI sequence is:

  1. create an empty environment with the oldest supported Python;
  2. install the pinned requirements;
  3. run pip check;
  4. run tests and a startup smoke test;
  5. repeat for the production Python version and operating-system image;
  6. build the deployment artifact from the verified inputs.

If one requirements file must support several platforms, environment markers can express legitimate differences. Packages with compiled extensions may still publish different wheels by Python ABI, operating system, or CPU architecture. A snapshot tested only on a developer laptop is not proof that a Linux container can install it.

Add integrity when versions are not enough

Exact versions improve repeatability, but they do not by themselves verify the downloaded artifact. pip's hash-checking mode requires approved hashes for every requirement:

example-package==1.2.3 \
    --hash=sha256:<approved-wheel-hash>
python -m pip install --require-hashes -r requirements.txt

Do not invent hashes or copy them from an untrusted build log. Generate and review them as part of the dependency-update process. Hash mode also requires complete transitive pins, which makes missing dependency decisions visible.

For controlled or offline deployments, pip can build a wheelhouse and later install only from that directory:

python -m pip wheel --wheel-dir wheelhouse -r requirements.txt
python -m pip install --no-index --find-links=wheelhouse -r requirements.txt

Build wheels for the same target Python and platform used in production. A wheel created for a different ABI or operating system may not be usable.

Do not confuse three different artifacts

ArtifactPrimary questionTypical owner
pyproject.toml dependenciesWhat does this project directly require?Library or application source
requirements.txtWhat concrete environment should pip install?Application deployment
pip freeze outputWhat distributions are installed here now?Diagnostic or snapshot workflow

One file can be used in more than one workflow, but the meanings do not become identical. A freeze file can serve as a pinned deployment input only after it is generated from a controlled environment, reviewed, and proven by a clean install.

Common failure modes

The wrong pip receives the install

Run python -m pip --version and print sys.executable. In automation, invoke .venv/bin/python or .venv\Scripts\python.exe directly.

The freeze file changes on another machine

Compare Python versions, OS and architecture, configured package indexes, and the original top-level requirements. Platform markers and available wheels can legitimately change the result.

A clean install succeeds but the application fails

pip install validates package metadata, not application behavior. Add imports, database migrations, service startup, and integration tests.

Updating one package rewrites half the file

Resolve from reviewed top-level inputs in a clean environment. Inspect why each transitive dependency moved, then test the whole environment. Avoid editing an unexplained freeze snapshot until it happens to install.

Requirements expose private infrastructure

Review direct URLs, editable paths, index options, credentials, usernames, and local filesystem paths before committing. Store repository credentials in the package manager or CI secret store, never in a requirements file.

A repeatable review checklist

  • The Python version is explicit and supported.
  • All pip commands run through that interpreter.
  • .venv is ignored and rebuildable.
  • Direct dependencies are distinguishable from the pinned environment.
  • Production and development dependencies are separated when appropriate.
  • The snapshot contains no credentials, private hosts, or local paths.
  • A second empty environment installs the file successfully.
  • pip check, tests, and a startup smoke test pass.
  • Target operating systems and architectures are covered.
  • Dependency changes are reviewed instead of silently regenerated.
  • Hashes or a wheelhouse are used when artifact integrity or offline installation requires them.

The important step is not the redirection operator in pip freeze > requirements.txt. It is proving that the recorded environment can be recreated from reviewed inputs on the systems where the software will actually run.

Official sources