Skip to main content
  1. Posts/

What are lock files and why use them ?

·3 mins
Pierre-Antoine Comby
Author
Pierre-Antoine Comby
PhD in AI and Neuroimaging
Table of Contents

If you’ve been working with modern Python packaging tools, you’ve probably seen files like uv.lock, poetry.lock or even pylock.toml (That’s PEP 751 btw), which looks like glorified requirements.txt. But requirements.txt is often replace with pyproject.toml files (and its nice dependencies group, PEP 621)

So you may asked1:

“Why do I need a lock file if I already declare dependencies in pyproject.toml?”

It’s a fair question — after all, both files seem to describe dependencies. But they actually serve different purposes.

pyproject.toml: Declaring What You Need
#

Your pyproject.toml is where you declare your project’s dependencies and constraints. For example:

[project]
dependencies = [
  "requests >=2.30,<3",
  "numpy >=1.25,<2"
]

This doesn’t mean you’ve chosen one specific version of requests or numpy. Instead, it says:

  • “I can work with any recent version of requests in the 2.x series.”
  • “I can work with any recent version of numpy before 2.0.”

In other words, pyproject.toml expresses what your project can work with.

Lock Files: Freezing the Exact Versions
#

A lock file (e.g. poetry.lock, uv.lock) pins every dependency, including sub-dependencies to a specific version:

requests==2.32.2
urllib3==2.2.2
numpy==1.26.4

This ensures that:

  • Everyone on your team, plus your CI/CD system, installs the same versions.
  • You don’t wake up to a broken build because a dependency released a buggy update.
  • Installs are faster, since the resolution step is skipped.

🍞 A Cooking Analogy
#

Think of it like following a recipe:

  • The recipe (pyproject.toml) says: “Use 2–3 cups of flour.” That’s a guideline — you have some flexibility.
  • The shopping list (lock file) says: “Bought 2.5 cups of King Arthur All-Purpose Flour, batch #XYZ.” That’s an exact record of what you used.

When you share the recipe, you don’t want to force everyone to buy your exact flour. But when you’re cooking together with friends or running the same dinner party in multiple kitchens, it’s nice to know exactly which ingredients were used so the results are reproducible.

But What About Libraries?
#

Traditionally, the advice has been:

  • Applications: Commit your lock file (for reproducibility).
  • Libraries: Don’t commit it (since end users shouldn’t be forced into your exact dependency tree).

That’s why many Poetry or PDM workflows don’t track lock files for libraries.

uv’s Perspective: Always Commit It
#

The new Python package manager uv takes a different approach. Its documentation recommends committing the uv.lock file even for libraries.

Here’s the reasoning:

  • The lock file is only for people developing the project, not for end users.
  • When someone installs your library from PyPI, they only see the pyproject.toml constraints — your lock file doesn’t ship with the package.
  • By committing the lock file, you guarantee reproducibility for contributors and CI environments, without constraining downstream users.

So in uv’s model:

  • pyproject.toml → the recipe (constraints you share).
  • uv.lock → the shopping receipt (exact versions you used and tested).

Takeaways
#

  • pyproject.toml defines constraints (“what you can work with”).
  • A lock file defines resolutions (“what you’re actually using right now”).
  • Tools like Poetry treat lock files as optional for libraries, but uv recommends always committing them for better reproducibility and speed.

In short: don’t think of the lock file as something you’re “leaking” to your users. It’s a tool for you and your collaborators — and in uv’s ecosystem, committing it is the way to go.


Would you like me to punch this up with a few CLI examples (uv add, uv sync) so readers see the lock file in action, or keep it more high-level and analogy-driven?


  1. And that was asking myself not so long ago ↩︎