I feel like the answer here depends a lot on the context.
One context is “using software”. For example, you want to use a tool, written in Python, and the tool has
import request inside and somehow assumes that the request Python package is available. In this cases, the software should packaged the nix-way, using a nix expression, and probably upstreamed to github.com/nixos/nixpkgs as well. You do want system-wide reproducibility and isolation for system-wide applications. Admittedly, the process of wrapping software which is not already in nixpkgs is annoying (as nixpkgs really wants to be a monorepo of all the things), but hopefully things will become easier in the flakes world.
Another context is “developing software”. If you are a Python developer, you might be working on three different projects, and each project has its own set of dependencies, etc. Here, I am pretty sure that language-specific build tool/package manager is the way to go, if the language has good tooling. I work with rust primarily, and
cargo are perfect for me to hack on my or other people’s projects. I don’t see how nix can add anything here. There’s also a fundamental tension here that, when you develop software, you need to handle both variability and reproducibility. For Rust, in Cargo.toml I specify effectively a range of versions of dependencies my project works with, but then I have a separate Cargo.lock which pins dependencies exactly. In this context, it’s important to make sure not only that the single point, Cargo.lock, is valid, but that the whole range works (using testing with deliberately older versions of libraries). The way I see it, nix is a very good tool to handle
Cargo.lock aspect of this workflow, but doesn’t have vocabulary for
Cargo.toml aspect. That said, when I need to use software written in rust (
exa), I install it using nix, as that falls into the first context.
The specific case of Python is different though – I haven’t been active in Python community for a while, but my understanding is that building & dependencies story doesn’t have a clear cut definitive answer. Virtual environments were added not from the start, and undergone several iterations, pip didn’t have (and still hasn’t?) lockfiles, etc. In this situation, it might be plausible that using
nix as a substitute for the language’s native infra would be beneficial. Though, I’d try to stick to default tooling nonetheless.
Finally, the third context is when you are developing software in a specific language, but it also depends on some system libraries. This is most typically manifested by having
apt-get install libfoo libXbar in project’s readme. For such cases, using nix to manage the extra dependencies is the right answer, because nix allows you to have such global deps isolated per project. When I need to work with such project, I either use direnv and a project-specific
shell.nix, or (if this is one off thing) I use my default
shell.nix which is just a grab bag of common system dependencies.