Q&A library Python

How do I make sure my Python project runs the same way next year?

Python Advanced dependenciespackagingreproducibility

Short answer

Pin exact versions in a lock file, not just loose ranges, so an upgrade to a dependency cannot silently change your results. Record the Python version too, and rebuild the environment from the file.

The short version

Declare loose requirements in pyproject.toml but pin exact versions in a lock file, and install from the lock file. Without pinning, a dependency releasing a new version can silently change your numbers between one run and the next.

The problem with loose requirements

dependencies = ["pandas", "scikit-learn"]

This says "any version". Install it today and next year, and you get different libraries. An API changes, a default changes, an algorithm is improved, and your analysis produces different results with no change to your code. That is not a hypothetical: default arguments in pandas and scikit-learn have changed several times in ways that alter output.

Pinning exactly

The simple approach captures everything currently installed.

pip freeze > requirements.txt

This records exact versions, including transitive dependencies you never named. Recreate it anywhere with:

pip install -r requirements.txt

Commit that file. It is the record of what actually produced your results.

The two-file discipline

The mature pattern separates two things.

Declare what you depend on, loosely, in pyproject.toml, so the project states its real requirements and can be upgraded deliberately.

Record what you installed, exactly, in a lock file, so any environment can be reproduced bit for bit.

Tools like uv, poetry and pip-tools manage this pair for you, generating the lock from the declaration and resolving the whole dependency tree consistently.

Pin the Python version too

Libraries are not the only thing that changes. Python itself changes defaults and behaviour between versions. Record the version your project expects.

[project]
requires-python = ">=3.11,<3.13"

And say in the README which version you actually used.

Upgrade deliberately, then test

Pinning does not mean never upgrading; it means upgrading on purpose. Bump the versions, rerun your tests, compare the outputs, and commit the new lock file when you are satisfied. That turns an upgrade from an accident into a decision.

This is exactly why tests on your data pipeline are worth writing: without them, you have no way to tell whether a library upgrade changed a result.

For an analysis that matters

If a number will be published or acted upon, record the lock file, the seed, the input data (or its checksum), and the code version, together. Any one of those four missing means the result cannot be reproduced. That is the standard, and it is not onerous once it is a habit.

Containers go further

A container image freezes the operating system and system libraries too, which matters for numerical work where a different compiled BLAS can change floating point results in the last decimal places. For most analysis this is more than you need, but for anything that must be exactly reproducible years later, it is the honest answer.

Need this solved on your own data?

The library is free and always will be. When the problem is bigger than an answer, we take on the work.

Start a project