One-time (or as-needed for manual upgrades):
1. Make a venv with setuptools, wheel, and pip-tools (to get pip-compile) installed.
2. Use venv's pip-compile to generate a fully-pinned piptools-requirements.txt for the venv.
3. Check piptools-requirements.txt into my repo. This is used to get a stable, versioned `pip-compile` for use on my payload requirements.
During normal development: 1. Add high-level dependencies to a `requirements.in` file. Usually unversioned, unless there's a good reason to specify something more exact.
2. On changes to `requirements.in`, make a venv from `piptools-requirements.txt` and its `pip-compile` to solve `requirements.in` into a fully-pinned `requirements.txt`.
3. Check requirements.in and requirements.txt into the repo.
4. Install packages from requirements.txt when making the venv that I need for production.
This approach is very easy to automate, CI/CD friendly, and completely repeatable. It doesn't require any nonstandard tools to deploy (and only needs pip-compile when recompiling requirements.txt). It also makes a clear distinction between "what packages do the developers actually want?" and "what is the fully-versioned set of all dependencies".It's worked great for me over the years, and I'd highly recommend it as a reliable way to use the standard Python package tooling.