How to pin specific package versions to avoid breakage during deployment

I want to ensure my Streamlit app runs consistently after deployment and doesn’t break when packages update.

  • What’s the correct way to pin versions in requirements.txt?
  • Should I match the versions from my local environment exactly?
  • Any best practices to avoid future compatibility issues

I spent many years as a senior eng on a release team, and my number one piece of advice is that you should use a system that allows you to specify your actual requirements in a flexible way and then compiles that into a full 100% hard-pinned and compatible dependency list of packages that will need to be in the deployed environment. This is built-in to both uv and poetry as export functionality, but is easy to accomplish even if you are just using requirements.txt and pip.

For example, specify your packages in a file named requirements.in (instead of the normal requirements.txt), and only specify the actual packages that are imported into your application code. You can specify and specific version restrictions on these packages as usual.

Then (after pip install pip-tools) you can compile that requirements.in file into a requirements.txt that will contain not just your specified dependencies, but also the hard-pinned versions of all transitive dependencies used by your dependencies. This is so critical, I can’t tell you the number of times I’ve seen an application break even when a user has fully hard-pinned all of their main dependencies but an issue occurred in an un-pinned transitive dep.

Here’s a real world example:
Deps specified in requirements.in:

And compiled into a full requirements.txt:

When you need to add a dependency, or are ready to upgrade to new versions, you simply do so and re-test the application. In between those times however you know you can reliably re-build the environment, deploy it, or otherwise ship it off and have a (mostly) reliably ability to re-deploy without unexpected changes.

Example of exactly how this file gets updated in this particular repo, using a pyinvoke task