A library should declare loose, tested bounds: a lower bound for the oldest version you support, and an upper bound only when you know a later release breaks you. Do not pin exact versions or list your dependencies' own dependencies. Pins belong in an application's lock file.
Two different jobs
The Python Packaging User Guide separates two things that people mix up. The dependencies in your pyproject.toml, historically install_requires, describe what a project minimally needs to run correctly. Requirements files, on the other hand, usually define a whole environment with exhaustive pinned versions for repeatable installs.
| Library dependencies | Requirements or lock file | |
|---|---|---|
| Purpose | What the project needs to work | Repeatable install of a full environment |
| Style | Abstract names and ranges | Exact pins, often with hashes |
| Read by pip automatically | Yes, on install | Only when you ask for it |
| Who owns it | Library author | Application owner |
What the guide recommends
For a library, it is best practice to indicate any known lower or upper bounds. If you know your project needs at least version 1 of one dependency, say so. If a dependency introduced a change in its version 2 that breaks you, it makes sense to exclude it.
[project]
dependencies = [
"package-a >= 1, < 2",
"package-b >= 2",
]
The guide also says it is not best practice to pin dependencies to specific versions or to list sub dependencies. That is overly restrictive and prevents users from gaining the benefit of dependency upgrades.
Why exact pins hurt your users
Imagine your library pins one package to a single version. An application that also uses a second library needing a newer version of that package cannot be installed. The resolver reports a conflict that neither you nor the application owner caused, only you can fix it. The more popular your library, the more projects get stuck.
Pins also delay security fixes. If a dependency ships a patch and you pin the vulnerable version, every user must wait for your next release. A range lets them upgrade the dependency themselves.
A practical method
- List direct dependencies only. Do not list what they depend on.
- Set each lower bound to the oldest version in your CI matrix that passes.
- Test against the newest release of each dependency too, in a scheduled job.
- Add an upper bound only when a specific release breaks you, and remove it once you fix the incompatibility.
- Keep a separate lock file for your own development and CI, outside the published metadata.
For compatible release syntax, see our post on version specifiers.
Optional features
Put dependencies needed only for a feature in optional extras so most users do not install them. The guide shows how these extras are declared and how an all extra can point back at your own project, which avoids maintaining the same list twice. See also extras versus dependency groups.
Why this is a supply chain topic
Everything you declare is a trust decision for your users. Each added dependency is another maintainer and another release stream. Fewer, well chosen, loosely bounded dependencies mean a smaller attack surface and faster fixes.
Try it yourself
Click through it at your own pace, or open it full screen.
Click through the inventory and notice how many packages arrive indirectly. That is the part your bounds influence the most.
Safeguard's SBOM and dependency scanning list direct and indirect components. You can run a free scan.
Quick check
Should my library pin exact versions?
No. The packaging guide says pinning exact versions or listing sub dependencies is overly restrictive. Use ranges, and keep pins in an application lock file.
When is an upper bound justified?
When you know a later major release breaks your code. Remove it as soon as you have fixed the incompatibility.
Where do exact pins belong?
In a requirements or lock file for a full environment, owned by the application that deploys it.
Sources
- install_requires vs requirements files, Python Packaging User Guide
- Writing your pyproject.toml, Python Packaging User Guide
Last reviewed: 19 October 2026.