- Published on
pip freeze Is Not a Lockfile: Reproducible Python Requirements
- Authors
- Name
- Code Mill Hub Editorial
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:
- Which Python interpreter is receiving the install?
- Which dependencies does the project intend to use?
- 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 pipor an explicit virtual-environment interpreter over a barepipcommand.- Create a disposable
.venv; do not commit or copy the environment directory.- Treat
pip freezeas a snapshot of everything installed, not as a declaration of direct dependencies or a solver-generated lock.- Rebuild from
requirements.txtin a new environment and runpip checkplus 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:
- create an empty environment with the oldest supported Python;
- install the pinned requirements;
- run
pip check; - run tests and a startup smoke test;
- repeat for the production Python version and operating-system image;
- 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
| Artifact | Primary question | Typical owner |
|---|---|---|
pyproject.toml dependencies | What does this project directly require? | Library or application source |
requirements.txt | What concrete environment should pip install? | Application deployment |
pip freeze output | What 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.
.venvis 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.
Related reading
Official sources
- Python
venvdocumentation - Python Packaging User Guide: install packages with pip and venv
- Python Packaging User Guide: writing
pyproject.toml - Python Packaging User Guide: project requirements versus requirements files
- pip freeze command reference
- pip repeatable installs guide
- pip install command reference