How Use Python on NixOS, the Pythonic Way
Written on
In a previous article I explained the Nix language using concepts familiar to Python developers, and I promised a follow-up on what it’s actually like to use Python for development on a NixOS box. Here it is.
If you come from a “normal” Linux distribution, you expect to install
Python, type pip install <something>, python, and get to work.
On NixOS that intuition breaks in surprising ways. Let’s look at why,
and then at an approach that gives you back the pythonic workflow you’re
used to.
The Options You (Think You) Have
When a pythonista lands on NixOS and asks “how do I get Python?”, they’re usually offered one of these answers:
- Put Python in your system configuration — add
python3toenvironment.systemPackagesin/etc/nixos/configuration.nix. - Put Python in your Home Manager configuration — add it to
home.packages. - Use a development shell — write a flake exposing a
devShelland enter it withnix develop(or the legacyshell.nixandnix-shell). - Use a Nix-built Python environment — declare
python3.withPackages (ps: [ ps.requests ... ])so that Nix builds an interpreter that already contains your dependencies.
All four are valid Nix. And all four, in their own way, fight the muscle memory of a Python developer. For most popular Python packages, there’s an equivalent Nix package you’re meant to install instead. And guess what: Python — the world’s most popular programming language — is so special in NixOS that some users suggest Docker (with a traditional distro) as an escape hatch!
Why the Nix-Way Feels Cumbersome
The Nix philosophy is wonderful: everything is declared, reproducible and
under version control. But Python’s packaging world was built around
a completely different assumption — that you install an interpreter once
and then create throwaway virtual environments on the fly, with pip or
uv, dozens of times, per project, per branch, per experiment.
Those two worldviews collide:
- A Nix-built Python environment is immutable. You can’t
pip installinto it. Want to try one more library? Edit a Nix file, rebuild, and re-enter the shell — for every single dependency change. That’s a hard sell for someone used topip install richand pressing Enter. - A
devShellis per-project and needs to be authored before you can experiment. Spinning up a quick scratch script in/tmpsuddenly requires writing Nix. - Putting
python3inenvironment.systemPackageslooks like the obvious move, but it gives you exactly one Python version for the whole machine, and it interacts badly with virtual environments, as we’ll see in a moment.
None of this is broken. It’s just deeply un-pythonic.
A Prominent Symptom: libstdc++.so.6
The collision becomes painfully concrete the first time you create a plain
virtual environment and pip install a package that ships compiled
extensions (NumPy, pandas, Pydantic, and a long tail of others):
$ uv init
$ uv add numpy
$ uv run python -c "import numpy"
Traceback (most recent call last):
...
ImportError: libstdc++.so.6: cannot open shared object file: No such file or directory
On Ubuntu or Fedora this would just work, because the C++ runtime lives in
a well-known location like /usr/lib and the dynamic linker finds it.
NixOS, by design, has no /usr/lib. Every library lives under its
own hashed path in the Nix store, and binaries are explicitly told
where to look. A wheel downloaded from PyPI knows nothing about that, so
the linker comes up empty-handed.
This single error is the rite of passage that sends countless newcomers to the forum in frustration.
How This Is Resolved the Nix-Way
The orthodox fix is to make the missing library available and tell the
dynamic linker about it. libstdc++.so.6 is provided by
stdenv.cc.cc.lib, so you write a flake that exposes a development shell
and enter it with nix develop:
flake.nix
{
description = "Python development shell";
inputs.nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
outputs = { self, nixpkgs }:
let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
in
{
devShells.x86_64-linux.default = pkgs.mkShell {
packages = [ pkgs.python3 ];
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.stdenv.cc.cc.lib ];
};
};
}
Or, to fix it system-wide for any foreign, dynamically-linked binary
(which is what PyPI wheels effectively are), you enable nix-ld in a
NixOS module — one wired into your flake’s nixosConfigurations — and
enumerate the libraries those binaries are allowed to find:
configuration.nix
{ pkgs, ... }:
{
programs.nix-ld = {
enable = true;
libraries = with pkgs; [
stdenv.cc.cc.lib
zlib
];
};
}
It works. But step back and look at what just happened: to import
numpy — the most ordinary thing a Python developer does — you had to
learn about flakes, mkShell, stdenv, LD_LIBRARY_PATH and the
Nix store, and you’ll carry a devShell in every project, re-entering it
with nix develop and editing the flake each time you add a dependency.
That is a lot of Nix to import a math library.
The nix-ld loader is the exception in that list: unlike the per-project
devShell boilerplate, you configure it once, system-wide, and
forget about it. Hold on to that thought — it’s the single piece of this
orthodox fix that survives into the clean approach below.
Hint
The error isn’t telling you that NixOS is wrong; it’s telling you that
you brought a non-Nix binary into a Nix world without a translator.
nix-ld is that translator — the question worth asking is whether
you needed a Nix-managed interpreter (and all its immutability) for that
binary in the first place, or just the translator.
The Mind Shift: Python Is a Development Package
Here is the insight that makes everything click, stated bluntly on the old NixOS wiki:
Python is a development package, and not meant to go in your system or home configuration.
For a pythonista that sentence is almost offensive at first — of course
Python goes on my system, how else would I run my code? But on NixOS,
“Python” is not your runtime in the way it is elsewhere. The Python that
runs your application is part of the application’s reproducible build (a
flake, a poetry2nix derivation, a container). The Python you use to
develop is a tool you reach for interactively, and it belongs to your
project and your workflow — not to the operating system.
So the recommendation is the opposite of what most newcomers do: keep
Python out of environment.systemPackages and out of
home.packages. Don’t let NixOS own your development interpreter.
Manage it locally, the way you used to manage interpreters with pyenv
(and nowadays with uv python) on any other machine — only better.
Use uv Instead
The tool that bridges the two worlds is uv. It’s written in Rust, so
it has no Python prerequisite of its own, it downloads
standalone interpreters for any version you ask for, and it manages
virtual environments and dependencies at the speed pythonistas wish pip
always had.
You install uv itself through Nix — that’s a system tool, not a
development package — and then let uv manage everything Python. Crucially,
you pair it with programs.nix-ld. As I argued in my Linux Day Prato
2025 talk, the recipe is “install only uv (but not any Python!)
via systemPackages“, and nix-ld is what makes that recipe actually work:
configuration.nix
{ pkgs, ... }:
{
environment = {
localBinInPath = true;
systemPackages = with pkgs; [ uv ];
};
programs.nix-ld = {
enable = true;
libraries = with pkgs; [
stdenv.cc.cc.lib
zlib
];
};
}
Don’t treat programs.nix-ld as optional here — it’s the load-bearing
piece. Everything uv hands you (its standalone interpreters and the
wheels you uv add/pip install into a virtualenv) is a foreign
binary that expects a normal Linux dynamic linker. Nix-ld provides
exactly that: a shim at the standard loader path that resolves the
libraries you list. With it in place, the libstdc++.so.6 rite of
passage from earlier simply stops happening, and import numpy behaves
like it does everywhere else.
With uv on your PATH, you manage Python versions yourself, on
demand, without touching a single Nix file:
$ uv python install 3.14 --default
$ uv python install 3.13 3.12
$ uv python upgrade 3.14
$ uv python list
cpython-3.15.0b2-linux-x86_64-gnu <download available>
...
uv also has a tool interface — the equivalent of pipx — for
installing Python-based command-line programs from PyPI without polluting
any project:
$ uv tool install ruff
$ uv tool install httpie
$ uv tool upgrade --all
$ uvx ruff check . # run a tool without installing it
These CLI starters land in ~/.local/bin. That’s exactly why we set
environment.localBinInPath = true above: without it you’d install a
tool successfully and then be greeted by a baffling “command not found”.
With all that in place, you can go ahead developing Python as you always
did, the modern way, using uv, of couse.
Hint
Notice the division of labour: Nix owns the box (the kernel,
drivers, system services and uv itself), while uv owns Python
(interpreters, virtual environments, dependencies and CLI tools). Each
tool does what it’s genuinely good at, and neither gets in the other’s way.
Enter Home Manager’s programs.uv.python and tool
Managing Python versions with uv python install by hand gives you
back some of the freedom you missed. But it’s imperative — you run
commands, and nothing records what you ran. That should make any NixOS
user slightly uncomfortable: the whole point of this distribution is that
your machine is declared.
The missing piece arrived with Home Manager PR #9507, which extends the programs.uv module, so you can declare your user-specific Python versions and tools, and Home Manager keeps them up to date for you:
home.nix
{
programs.uv = {
enable = true;
python = {
versions = [ "3.14" "3.13" "3.12" ];
default = [ "3.14" ];
prune = true;
};
tool = {
packages = [ "ruff" "httpie" "pre-commit" ];
prune = true;
};
};
}
This is the best of both worlds:
python.versionsrequests are passed verbatim to uv, so you keep uv’s flexible version syntax while declaring it in Nix.python.defaultmarks which interpreter answers a barepython.prune = trueremoves versions (and tools) you’ve taken off the list, so your configuration is the single source of truth.- On activation, Home Manager runs
uv python upgradeanduv tool upgrade --allfor you, so your interpreters and CLI tools stay patched without manual intervention — declarative and current.
You get a fully declarative, version-controlled Python setup that a
seasoned NixOS user expects, expressed entirely in terms a Python developer
already understands. No mkShell boilerplate to import NumPy, no
rebuild-per-dependency, no libstdc++ rabbit hole — just uv, managed
the Nix way at exactly the layer where it belongs.
Recap
NixOS and Python pull in opposite directions: one wants everything
immutable and declared, the other wants fast, throwaway, interactive
environments. The reconciliation is a mind shift — Python is a
development package, so keep it out of your system and home
configuration, install uv through Nix, and let uv own your
interpreters and tools. Enable environment.localBinInPath so the CLI
tools you install are actually found, and finally lift the whole setup into
programs.uv in Home Manager to make it declarative and self-updating.
The result is a genuinely pythonic work environment, running on the most reproducible Linux distribution there is.
Famous Last Words
Note that this is not to say, you should fight the Nix way. The described
approach gives you back the pythonic way of working on Python projects.
In the long run, you’ll get used to the Nix way of doing things, and
you’ll start appreciating its elegance. When that kicks in, you’ll see
yourself equipping your projects with a flake.nix file that gives
everyone on your project a development shell with everything prepared
to start working immediately.
This is when you’ll start wondering, whether you should install anything system-wide on your laptop, because you can install most of the things you need on demand with your development shell, and the rest user-specific with Home Manager. With Nix, this is possible. Nix gives you the choice.
You’ll be ready to embrace the power of Nix — when the day comes.
Resources
- Python on NixOS (old wiki) — “Python is a development package”
- Python on NixOS (wiki.nixos.org)
- How are you ‘supposed’ to install Python packages? (NixOS subreddit)
- Python on Nix – tutorial by F. Ridh
- Python development with Nix (dev.to)
- PyCon 25: Python on NixOS (Slides)
- Linuxday 2025: I want a pythonic work environment (Slides)
- Home Manager PR #9507: uv python and tool management
- Painless NixOS-Config repository
(GitLab) — show-cases
programs.uv,pythonandtools - NixOS-Config generator (Copier template) — scaffolds a nixos-config
