Skip to content

Package Pinning

Package pinning fixes dependency versions for a quant run. Learn how exact Python pins differ from ranges, package inventories and a complete reproducibility record.

Package pinning means requiring a dependency to use a specified version, rather than allowing it to move to any newer acceptable release. In a quant project, pins help preserve the numerical software used for a valuation or backtest, so a later installation does not silently select different package versions.
Pinning is one part of dependency management. It constrains which versions may be installed; it does not establish that the model, data or resulting numbers are correct.

Read the version requirement carefully

In Python packaging, a conventional exact version pin uses == without a wildcard. These requirements express different policies:
RequirementWhat it permits
numpy==2.5.1The specified public version
numpy==2.5.*Versions matching the 2.5 prefix; this is not an exact pin
numpy~=2.5.1Compatible-release range: at least 2.5.1, within the 2.5 release series
numpy>=2.5.1A lower bound, with no upper bound
These are version-selection rules, not evidence that every permitted release exists or works with your code. Python version comparison also normalizes release segments; a public-version requirement can accept a matching local build label. For example, ==2.5.1 also matches 2.5.1+desk.1. A version pin therefore does not identify a unique downloadable file. See PyPA's version-matching specification.

Exact pins are especially useful for replaying a particular application or research run. A reusable library usually needs tested compatibility ranges instead: forcing every downstream user onto the author's exact versions makes upgrades and security fixes harder.

Observe first, then record

The following code reads the selected interpreter and installed distribution metadata. It was executed with Python 3.14.4, NumPy 2.5.1 and SciPy 1.18.0. These are observed versions, not a claim that they are the latest releases or the right choices for every project. Your output may differ.
import platform from importlib.metadata import version print(f"Python {platform.python_version()}") for package in ("numpy", "scipy"): print(f"{package}=={version(package)}") # Python 3.14.4 # numpy==2.5.1 # scipy==1.18.0

The final two lines could be package requirements in a pip requirements file. The Python line is a separate interpreter record; it is not a pip dependency. No installation is performed by this example.

Observed Python 3.14.4 is recorded separately; NumPy 2.5.1 and SciPy 1.18.0 give numpy==2.5.1 and scipy==1.18.0. Those package pins leave dependency completeness, artifacts, platform and run inputs to be recorded.
Observed Python 3.14.4 is recorded separately; NumPy 2.5.1 and SciPy 1.18.0 give numpy==2.5.1 and scipy==1.18.0. Those package pins leave dependency completeness, artifacts, platform and run inputs to be recorded.
Observed versions distinguish the interpreter record from package pins. Two package lines do not capture the whole calculation.
A pip requirements file is an installation input, not automatically a complete environment specification. It may contain unpinned names, ranges, exact pins, direct references or pip options. Its filename alone says nothing about how tightly the environment is controlled. The requirements-file reference defines that format.

An inventory is useful; check what it represents

pip freeze reports installed packages in requirements format. It can capture installed transitive dependencies as well as the packages you chose directly. However, it describes the environment you already have: that may include unrelated exploratory packages. It does not calculate a lockfile or a dependency-solver result.
Defaults also matter. On Python 3.12 and later, pip freeze omits pip itself; --all includes the otherwise omitted packaging tools. Neither output records the interpreter version, operating system or market-data snapshot. These details follow the checked pip 26.2.2 documentation; the inventory command was not run in the example above.

Preserve the rest of the run

Direct pins leave unlisted transitive dependencies free to vary within their requirements. Record the complete resolved dependency set for the intended environment. To identify approved distribution artifacts, add verified hashes: pip's hash-checking mode requires all requirements and dependencies to be explicitly pinned and hashed. Retaining the artifacts also helps when their original source becomes unavailable. See secure installs and repeatable installs.
A compiled wheel can depend on a particular Python ABI, operating system and architecture (see platform compatibility tags); native numerical libraries and execution settings can affect results too. Matching package versions across machines does not by itself promise identical floating-point output.
For a reproducible environment, retain the interpreter and platform details alongside code revision, configuration, original data identifiers and any random seeds. Use a virtual environment to isolate packages, and test the replay against an appropriate numerical tolerance. When upgrading, update the recorded environment deliberately and rerun the relevant tests: a pin preserves a choice, including an obsolete or faulty one.
For the practical workflow, Virtual Environments covers interpreter selection and isolation; Reproducibility builds a run record. Both have public previews; their full lessons require membership.

Sources

Last updated
  • python
  • dependencies
  • reproducibility
  • software engineering
← Back to the glossaryBack to top ↑