Your package calls a function that example-lib added in version 2.1. So in
pyproject.toml you write:
[project]
dependencies = ["example-lib>=2.1"]
>=2.1 is a version specifier, a rule that says which versions of a
dependency your package accepts. The installer picks a version that
satisfies it. Versions compare as numbers, one segment at a time, not as
text:
assert "1.10" < "1.9" # as text, "1" sorts before "9"
assert (1, 10) > (1, 9) # as numbers, 10 is greater than 9
Read a specifier
A specifier is one or more clauses joined by commas, and a version must satisfy every clause:
| Specifier | Allows |
|---|---|
>=2.1 |
2.1 and anything newer |
>=2.1,<3 |
2.1 and newer, up to but not including 3.0 |
==2.4.7 |
Only 2.4.7 |
>=2.1,!=2.3.0 |
2.1 and newer, except 2.3.0 |
Missing segments count as zero, so 1.0 and 1.0.0 are the same version.
The compatible-release operator ~= combines a lower bound with a fixed
prefix:
| Requirement | Same as |
|---|---|
~=1.4.2 |
>=1.4.2, ==1.4.* |
~=1.4 |
>=1.4, ==1.* |
So 1.5.0 satisfies ~=1.4 but not ~=1.4.2. The prefix is the version you
wrote minus its last segment. That last segment, and any after it, can grow.
Check your understanding Which versions satisfy ~=2.1: 2.1.0, 2.4.7, 3.0.0 or 2.0.9? Show answer Hide answer
2.1.0 and 2.4.7. Both are at least 2.1, and both start with the prefix
2. 3.0.0 has the wrong prefix, and 2.0.9 is below the lower bound.
Choose the bounds
Set the lower bound at the first release that has what you need, such as the function you call or a bug fix you rely on.
Add an upper bound, such as <3, only for a reason you can name. Maybe you
know that 3.0 breaks your code, or the project says it breaks its API in
every major release. Write the reason in a comment next to the line, and
widen the range once you have tested the new release.
Both directions cost something. A range that is too wide lets in a release that breaks your code. A range that is too narrow stops your users from installing your package next to another package that needs a newer version. A version number alone does not tell you that two releases are compatible, so test new releases.
When two packages ask for ranges that do not overlap, such as
example-lib>=2,<3 and example-lib>=3, no version satisfies both. The
installer's resolver, the part that picks one version of each package,
then tries other versions of those two packages. It fails if none of them
fit together.
Give a library wider ranges than an application
An application installs exact versions from a lockfile, which a later article covers. It still needs ranges, because they decide which versions an update can move to. A library is different. Its users install it next to their own packages, and their installer reads your ranges. They never see your lockfile. So give a library the widest ranges you can support, and test more than one combination.
Apply the upper-bound rule to requires-python too. If your package needs
Python 3.14 or newer, write requires-python = ">=3.14". Don't add a bound
such as <3.16 only because you have not tried 3.16 yet. pip would then
refuse to install your package on Python 3.16, even if it works there.
Going further
Real version numbers can also carry prereleases such as 2.0rc1,
post-releases, epochs and local labels. In production code, compare versions
with the packaging library instead of your own parser. The exercise below
handles release numbers only, so you can write the comparison yourself.
Some tools let you override a dependency's declared range, such as uv's dependency overrides. An override replaces the requirement but does not make the combination work, so test it and write down why it exists. See the version specifier specification.