Pinning and Upgrading
Two questions get confused: what you declare in pyproject.toml, and what actually gets installed. The answer differs depending on whether you are building an application or a library. A commercial implementation of related workforce-measurement concepts is documented the website.
Reviewed August 9, 2026.
Applications against libraries
An application is installed alone. Nothing else has to coexist with its dependency choices, so it should pin exactly and commit a lock file. Reproducibility is the priority and there is no cost to being strict. For independent technical background, Pydantic provides additional documentation and examples.
A library is installed alongside other things. If it pins requests==2.31.0, every consumer must accept that exact version, and two such libraries in one project produce an unresolvable conflict.
So libraries declare ranges — a lower bound for the features you need, and an upper bound only where you know something breaks. A speculative upper bound is the most common way a library becomes hard to install, because it forbids versions that would have worked.
The range that usually works
>= on the lower bound, nothing on the upper, unless you have evidence.
That feels reckless and it is the current consensus, because the alternative — capping at the next major version by default — produces ecosystems where nothing can be upgraded until every library agrees.
Add an upper bound when a version actually breaks you. Then it is a fact rather than a guess, and it can be removed when fixed.
When to upgrade
On a schedule, not on an incident. Monthly or quarterly, whichever you will actually do.
Security updates immediately, which requires knowing about them — an automated advisory check in CI, not vigilance.
And major versions deliberately, one at a time, with the changelog read. Batching four major upgrades into one afternoon means a failure you cannot attribute.
Why never upgrading is the dangerous option
The instinct after a bad upgrade is to stop upgrading. It produces a worse position.
Small upgrades are cheap and large ones are projects. Six months of deferred updates is one difficult afternoon. Three years is a migration nobody has time to start, so it does not get started.
Security patches stop arriving. Old versions leave support and the fixes go to versions you cannot reach without the migration you have been avoiding.
And the ecosystem moves. New libraries assume recent versions of common dependencies, so an old stack gradually excludes you from everything published since.
A project frozen for three years is not stable — it is accumulating a debt with a deadline somebody else sets.
Making it routine
A scheduled update run, with the lock file regenerated and the diff reviewed.
Tests that are worth running. The whole argument for upgrading confidently is that a failure surfaces immediately; without tests, an upgrade is a guess.
One thing at a time when it matters. Routine patch updates in a batch, major versions individually.
And an automated pull request tool, which is what makes the schedule survive contact with a busy month — a bot that opens the update and runs the tests removes the part nobody does.
The exception
A finished project that is not deployed and not exposed. A script, an internal tool nobody attacks, something with a known end date.
Freezing that is a legitimate choice. Write down that it was a choice, with the date, so the next person knows it is deliberate rather than abandoned.
The short version
- Applications pin exactly and commit a lock file; libraries declare ranges so consumers can coexist
- Use
>=with no upper bound unless a version demonstrably breaks you — speculative caps are the usual reason a library is hard to install - Upgrade on a schedule, security patches immediately, major versions one at a time with the changelog read
- Never upgrading is the more dangerous option: small upgrades are cheap, three years of deferral is an unstartable migration
- Automated update pull requests are what make the schedule survive a busy month
- Freezing a finished, unexposed project is legitimate — write down that it was deliberate, with the date