Safeguard
Developer Tools•

What does a pinned dependency actually mean in each ecosystem?

Pinned means different things in pip, NuGet, Gradle, Composer and npm. Use this table to agree on one team definition and stop false confidence.

Safeguard Security Team
4 min read

A pinned dependency is one where the exact version cannot change without a deliberate edit. But the word hides differences. In pip it means an exact version with an equals sign. In NuGet, a plain version number is actually a minimum. In Gradle, a dynamic selector floats until you lock it. Agree on a team definition that works in all of them.

Why one word causes trouble

When a Python developer says "we pin everything" and a .NET developer says the same, they may mean different things. Both can be sincere. One is describing exact versions, the other is describing a number in a project file that the resolver treats as a floor.

A mismatch like that shows up in incident reviews as "we thought that was fixed". A shared glossary prevents it.

What the docs say per ecosystem

EcosystemA plain version in the manifest meansWhat makes the result fixed
pipHash checking requires an exact equals version, a URL or a pathPinned versions plus hashes
NuGetThe version or higher, with preference for the lowestLock file, restored in locked mode
GradleDynamic selectors such as 5.+ are allowedDependency locking, written to gradle.lockfile
ComposerA constraint, resolved against composer.jsoncomposer.lock, used by install
npmA range or version in package.jsonpackage-lock.json, used by clean install

Notes: the NuGet docs say 3.6.0 means any version at or above 3.6.0, preferring the lowest, and they describe floating versions such as 3.6.* separately. Gradle's locking exists so builds use the same versions even with dynamic selectors. For pip, the secure installs page says every requirement must be pinned to a URL, a path or an exact version for hash checking.

Three levels worth naming

Use words that map to what you can verify:

  1. Declared: the manifest names a version or range.
  2. Locked: a lock file records the resolved graph, including transitive packages, and CI installs from it.
  3. Verified: the lock also carries hashes, and the installer checks them.

Most teams are at level 2 in some languages and level 1 in others. That is fine as long as the table is honest.

A short audit

For each repo, answer in one line:

  • Which level is it at today?
  • If it is below level 2, what stops it from getting there?
  • Is the install step in CI actually using the lock?

The third question catches a common gap: a lock file exists, but CI runs an ordinary install that rewrites it.

Why transitive packages decide it

Pinning your direct dependencies is the visible part. The transitive graph is larger and is where surprises come from. NuGet's docs give an example: you declare version 4.0.0 of a package, only 4.1.0 and later exist, so you resolve 4.1.0. The next day 4.0.0 is published, and NuGet starts resolving 4.0.0. Nothing in your project file changed. A lock file removes that surprise.

Safeguard can help here: it scans dependencies and generates SBOMs, applies reachability analysis so you can sort findings by what your code can actually call, and enforces policies and gates on pull requests. You can try a free scan on a repository to see what it finds.

Quick check

Is a version number in package.json or a project file a pin?

Not on its own. It is a declaration. The lock file is what fixes the resolved versions, and only if CI installs from it.

Do I need hashes as well as a lock?

Hashes add integrity checking. pip makes them all or nothing, which is strict but clear. Treat hashes as the verified level where the ecosystem supports them.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.