Pinning Python requirements

== vs >= vs ~= in requirements.txt, what each one resolves to, and why the Python app's install-on-every-start makes pinning matter more here than on your laptop.

Your requirements.txt doesn't just list packages — the version rules next to each name decide exactly what gets installed. Get them right and your app installs the same versions every time; get them loose and a dependency can quietly jump to a new major release and break your app overnight. This matters more on Falix than most places, because the Python application re-runs the install on every start. This guide covers each version operator, what it actually resolves to, and how to pin for reproducible installs.

At a glance
You need A Falix server running the Python application
Plan Any
Time Fifteen minutes

Why pinning matters more here

On start, the Python app installs your requirements with an upgrade flag — roughly pip install -U --prefix .local -r requirements.txt. The -U is the important part: for any package whose version rule allows something newer, pip pulls the newest version that still satisfies the rule, every single start. So a loose rule doesn't just affect the first install — it can hand you a different version each time the server restarts.

That's convenient when you want the latest patch, and a trap when a new release changes behaviour under you. Pinning is how you decide which it is.

The operators, and what they resolve to

Say a package demo has published versions 21.0, 21.3, 22.0, and 26.2 (newest). Here's what each rule installs:

In requirements.txt Means Installs
demo==21.0 Exactly this version 21.0
demo>=21.0 This or anything newer (no upper bound) 26.2 (the newest)
demo~=21.0 "Compatible release": >=21.0, <22.0 21.3 (newest 21.x)
demo~=21.3 >=21.3, <22.0 21.3
demo>=21.0,<23 An explicit range 22.0 (newest under 23)
demo No rule at all 26.2 (the newest, and re-checked every start)

The one people misread is ~= (compatible release). It pins the left-most part and lets the rest float:

  • ~=21.0>=21.0, <22.0 — patch and minor updates within 21, but never 22.
  • ~=21.3>=21.3, <22.0 — same upper bound, higher floor.
  • ~=21.3.1>=21.3.1, <21.4.0 — with three parts, only the patch floats.

🎯 Good to know: >= has no ceiling. flask>=2.0 will happily install Flask 3.x — a different major version — the next time a new one ships and your server restarts. If you don't want major bumps, use ~= or an explicit upper bound instead.

How to pin, in practice

Match the rule to how much change you'll tolerate:

# Exact — the safest, most reproducible. Nothing moves until you change it.
discord.py==2.4.0
flask==3.0.3

# Compatible releases — get bug fixes, never a surprise major.
requests~=2.32
python-dotenv~=1.0

# Only where you genuinely want the latest and accept the risk:
some-tool>=1.0

For a project you care about keeping stable, pin exact versions (==). It's the difference between "it installed the same thing it did last week" and "it installed whatever was newest this morning."

Getting a known-good set

Install and test your app until it works, then capture the exact versions you have. Locally, pip freeze prints every installed package at its exact version — paste the lines you depend on into requirements.txt. On Falix, the Packages page shows each dependency's Requested / Installed / Latest versions side by side, so you can see at a glance where a loose rule has drifted ahead of what you pinned.

Where the packages live

Installed packages go into a .local folder in your server, which isn't yours to keep — a reinstall or an application switch wipes it. You never rebuild it by hand, though: the install runs again on the next start and restores everything from requirements.txt. That's the whole reason the file is the source of truth — guard it, not the installed folder. (Your own data belongs in a managed database, not in .local.)

Troubleshooting

  • ERROR: Could not find a version that satisfies the requirement ... — the version rule can't be met (a typo'd name, or a version that doesn't exist). Check the exact spelling and that the version is real. See Install failures.
  • Two packages demand conflicting versions — pip reports the clash during install. Loosen one pin (often ~= instead of ==) so a common version exists, and restart.
  • "It worked last week, now it's broken" with no code change — a loose rule pulled a newer version on restart. Pin the working versions with == and it stops moving.
  • Managing dependencies from pyproject.toml (poetry/uv)? — that's a different flow; see Poetry and uv on Falix.

The full version-specifier syntax (including !=, .*, and pre-release rules) is documented at pip.pypa.io.


Next steps

Was this guide helpful?