Why is the output of `pip freeze` not a lockfile?
answer
- It reports a state, not a decision
- One column of text, nothing else
- No graph, no markers, no hashes
- It also captures whatever you installed by hand
- Editable installs freeze as local paths
basics
~20 spip freeze prints a flat list of whatever is installed in one environment: names and exact versions only. It carries no artefact hashes, no environment markers and no dependency graph, so it cannot reproduce a resolution elsewhere.
solid answer
~50 s`pip freeze` inspects the installed distributions and prints `name==version` for each. That is a **snapshot of an environment**, not a record of a resolution, and four things are missing. There is no dependency graph, so you cannot tell a direct dependency from a transitive one and cannot re-resolve later. There are no environment markers, so a file frozen on one operating system and interpreter version may name distributions that are wrong, or unavailable, elsewhere. There are no artefact hashes, so nothing verifies that the bytes you install are the bytes that were resolved. And there is no provenance -- no index the artefacts came from. It also faithfully captures pollution: anything installed by hand, plus editable installs that emerge as `-e` lines pointing at local paths. A real lockfile records the resolved graph with markers, hashes and provenance, keyed to the abstract inputs it came from.
code
console · 1 linepython -m pip freeze > requirements.txtgo deeper
Know that pip freeze prints what is installed right now, one name==version per line, and that committing it captures anything you installed by hand. Recall that it says nothing about where packages came from.
Explain the four missing pieces -- graph, markers, hashes, provenance -- and what each one costs you in practice. Be able to describe how a compiled requirements file differs from a frozen one.
Demonstrate the operational consequences: environments that drift, images that build differently on two machines, and dependencies nobody can safely delete. Show how you would migrate a frozen file to a real lock without a flag day.
Own the standard across repositories: which lock format, who regenerates it, how staleness is detected, and how much tool lock-in a portable interchange format is worth. Weigh the cost of enforcing one toolchain against per-team choice.
### What `pip freeze` actually does It walks the distributions installed in the current environment, reads each one's recorded name and version, and prints `name==version` lines. That is the whole algorithm. It never consults pyproject.toml, never runs a resolver, and never looks at an index. It reports a **state**, after the fact. ### What a lockfile has to carry A lockfile is the durable output of one resolution, and to be worth the name it needs: * **The resolved graph, with edges.** Which requirement pulled in which package. Without it you cannot drop a dependency and re-resolve, and you cannot audit why something is installed. * **Environment markers.** A single lock that serves Linux, macOS and several interpreter versions has to say *this wheel only when `sys_platform == "linux"`*. Otherwise you need one lock per environment. * **Artefact hashes.** Version numbers name a release; hashes name the bytes. * **Provenance.** The index or URL each artefact came from. * **A link back to the inputs.** The abstract requirements the lock was resolved from, so tooling can tell you the lock is stale when pyproject.toml changes. Frozen output has none of these. It is one column of text. ### The concrete failure modes **Environment pollution.** Freeze reports everything installed, including the debugging tool you pip-installed once at 2am and the leftovers of a package you removed from the project but never uninstalled. Those become permanent deployment dependencies the moment you commit the file. **Direct and transitive are indistinguishable.** Six months later nobody knows which of the sixty lines you actually import. Dropping a dependency becomes guesswork; `pip list --not-required` gives a partial hint by showing distributions nothing else depends on, but it is a heuristic, not a graph. **Platform and interpreter bleed.** Freeze on Linux and you may capture a distribution that only exists for Linux, or a version whose wheel is not published for macOS. Installing the file elsewhere either fails or silently builds from source. Nothing in the file records the environment it was taken from. **Editable and direct-URL installs.** A project installed with `pip install -e .` does not come out as a version pin. It comes out as an `-e` line naming a local filesystem path, which means nothing on another machine or inside a container. `pip freeze --exclude-editable` suppresses those lines -- which is a way of losing the dependency, not of recording it. **No integrity.** Two installs of the same pinned version can differ if the artefact was replaced on an index or a mirror. Only hashes catch that. ### The ladder from freeze to a lockfile 1. **`pip freeze`** -- a snapshot. Fine for reporting what is in a container you are debugging; not an input you should build releases from. 2. **A compiled requirements file** (for example `pip-compile` output) -- a real resolution of your abstract requirements, pinned, annotated with which requirement pulled each package, and optionally generated with hashes for `--require-hashes`. Still typically one file per platform, because a plain requirements file expresses markers awkwardly. 3. **A tool-owned lockfile** -- `uv.lock` or `poetry.lock`. Multi-platform through markers, hash-verified, tied to the abstract inputs, and regenerated rather than edited. The cost is that the format belongs to one tool. 4. **`pylock.toml`** -- the standardised, tool-neutral lock format defined by PEP 751, which packaging tools are progressively adopting as an interchange and export target. ### How to answer the practical version of this question If someone hands you a `pip freeze` output and calls it a lockfile, the fix is not to reformat it. Write the abstract requirements down in pyproject.toml, resolve them with a tool that produces a real lock, and treat the pinned artefact as generated: regenerate on upgrade, review the diff, and let CI install from it rather than resolve afresh on every run.
- What does `pip freeze` emit for a project installed with `pip install -e .`?Not a version pin. It emits an `-e` line naming the local filesystem path the project was installed from, preceded by a comment noting there is no version control information. That line is meaningless on any other machine or in a container image, which is why teams either strip it with `--exclude-editable` -- losing the dependency entirely -- or stop using freeze output as an install input.
- How would you produce a pip-installable pinned file that still records why each package is present?Compile it from the abstract requirements rather than freezing the environment. A requirements compiler resolves pyproject.toml's dependencies once and writes a pinned file annotated with the requirement that pulled in each package, and it can emit `--hash` lines at the same time so the file works under `--require-hashes`. Regenerate it on every upgrade and commit the diff for review.
- Why does a pinned file frozen on Linux sometimes fail to install on macOS?Because the file has no environment markers. Freeze records the distributions that were installed on that machine, including any that exist only for that platform, and pins versions whose wheels may not be published for another platform or interpreter version. Installation then either errors or falls back to building from a source distribution. A lockfile solves this by attaching markers to each entry so one file serves several environments.
A photograph of your desk at five o'clock is not a build plan. It shows what happens to be lying there, not what was ordered, why, or from whom.
saying these in an interview costs you the question
- Calls pip freeze output a lockfile
- Thinks exact versions alone guarantee a reproducible install
- Forgets freeze captures hand-installed tools and leftovers
- Assumes one frozen file works on every OS and interpreter version
- Believes freeze records which package required which
- Hand-maintains frozen output instead of regenerating it