Python Package Managers: Uv vs Pixi?”:

Python is a special language where it’s extremely popular to write libraries of code in compiled languages like C, C++, Fortran or Rust and bind them into Python. While Python is a relatively slow language it can call into these fast compiled dependencies and use them in the same way it can use Python dependencies. Many languages can do this, but this practice has taken off hugely in the Python community because it allows users to trade off performant code with a friendly and flexible programming language and often get the best of both worlds.

One big problem with pip in the early days was that it only handled source distributions. This means it could download a gzip file of source code and put it in the right place, call some hooks that was it.

The conda package manager handles a different kind of package. While you can still put pure Python code into a conda package you can also include pre-compiled binaries. When you build a conda package you run the compiler for all the common operating systems you expect it to run on, Windows, Linux, macOS and the common CPU architectures like x86 and ARM. This is a lot more work for the developers to build all these packages, but it hugely simplifies things for the end user as conda can just download the right binaries for their system without needing to compile anything.

Another thing conda does differently is it can look at your computer and find things that have been installed by other means through virtual packages. Nearly all compiled code depends on core libraries like glibc or musl which are included with the operating system, conda can figure out what versions of these packages you have and then include that in it’s package dependency solve. This has been especially useful in the CUDA Python ecosystem where all Python CUDA packages depend on specific NVIDIA GPU driver and CUDA versions.

One interesting consequence of conda supporting compiled binaries as well as source code is that it can package code written in any language, and compiled for any target hardware. This has resulted in conda becoming a popular package manager in the R and Julia communities. You can also use it to install entire other ecosystems like nodejs or golang. You can even package Python itself as a conda package and install it with conda.

In the years since conda was created the pip community have added binary packages. We now have the Python wheel which is a new package type that allows you to include compiled code and publish it on PyPI. This means pip no longer needs to compile these dependencies locally, closing this feature gap with conda. However, the conda community is now big enough and mature enough that it has momentum and will likely not go away despite this early unique selling point becoming irrelevant.

There are some differences between wheels used by pip and conda packages used by conda. One being that wheels must include all compiled dependencies in a statically linked binary.

Another topic commonly discussed in package management communities is locking. Once you’ve solved your environment you can store that solve somewhere in your project so that you don’t need to do it again unless you specifically need to upgrade something. This is helpful because if you do the same solve again next week there may be new packages released which affects the results of the solve. For reproducible environments having a lock file is critical, whether you want to reproduce a science experiment 5 years from now, or just be confident that your production web server isn’t going to pick up a new version of something and break unexpectedly.