Modernizing Python Virtual Environments with UV and Nix
This article is me adapting my older technique of creating a Python virtual
environment under Nix using the more traditional pip
and virtualenv
programs
to the cool new uv
program, a re-write in Rust, that’s appearing in all the
new AI tools like MCP and Manus. If you’re just here to grab the code so you can
bottle up your own multi-platform AI packages, you can get the flake.nix
at my
Python Nix Flake repo on GitHub.
Simplifying Complex Installations
It’s really just a starting point for bottling-up these difficult installs into 2 commands (one to install nix and another to activate the app. This article documents the conversion.
Lessons from MCP: When Going Against the Grain Goes Wrong
This weekend was a bust figuring out Anthropic’s MCP (model context protocol)
because I went against the grain, 3-fold. I am not using a Mac or Windows
desktop and I insisted on using pip
instead of uv
. And finally, I tried
getting rid of the Pedantic MyPy static data typing style. I also considered
making it synchronous, haha!
I did all this to try to distill the MCP example down to as simple a “Hello World” of MCP as I could, but it was a total bust. The documentation turned out to be too out of sync with the PyPI and the GitHub distros. I can only imagine it’s working for so many people out there writing MCP servers for AI editors by following the tutorials very closely.
Pivoting to OpenManus: The Latest AI Agent Platform
Anyway, I got frustrates and am putting MCP aside for now and taking up the other big thing ligting up the X-sphere, Manus. Or more precisely, OpenManus. So what happened?
Another Chinese cloud based app (echos of DeepSeek) put out the next thing just last Thursday, March 6th, that all the cool AI bros are flocking to right now. But it is by invitation only. Funny thing is that the open source version of it, OpenManus, popped up as fast as it was announced.
Understanding Manus and OpenManus
So what is Manus (and OpenManus)? It looks a bit like OpenAI ChatGPT’s Orchestrator that does browser automation plus coding. It’s also like Goose AI, an open-source AI agent platform released by Block that I tried a few weeks ago. The idea is that these platforms don’t wait for you to prompt them. They take your initial prompt and create a feedback loop, taking the AI’s response and feeding it back in as a new prompt until the task is complete.
Embracing UV for Better Dependency Management
Now this is a bandwagon I don’t want to miss out on merely because I go against the grain. Quite the contrary, my “nix” way of operating nixes so much of the difficulty of doing the installs of these things that the time is right for me to just get over the hump and put out a super nix flake that does one of these agentic AI platforms soup-to-nuts.
So, two projects in a row that choose uv
over pip for dependency control. That
should go hand-in-hand with nix super-well. But I have to test it. So instead of
this article being about the agentic platforms (just yet), it’s about me
switching my cross-platform Python Nix Flake that uses pip
and virtualenv
separately into one that uses nv
.
Here’s the before:
# ____ _ _ .--. ___________
# | _ \ __ _ _ ____ _(_)_ __ (_)_ __ ,--./,-. |o_o | | | |
# | | | |/ _` | '__\ \ /\ / / | '_ \| \ \/ / / # \ |:_/ | | | |
# | |_| | (_| | | \ V V /| | | | | |> < | | // \ \ |_____|_____|
# |____/ \__,_|_| \_/\_/ |_|_| |_|_/_/\_\ \ / (| | ) | | |
# `._,._,' /'\_ _/`\ | | |
# Solving the "Not on my machine" problem well. \___)=(___/ |_____|_____|
# Most modern development is done on Linux, but Macs are Unix. If you think Homebrew and Docker
# are the solution, you're wrong. Welcome to the world of Nix Flakes! This file defines a complete,
# reproducible development environment. It's like a recipe for your perfect workspace, ensuring
# everyone on your team has the exact same setup, every time. As a bonus, you can use Nix flakes on
# Windows under WSL. Plus, whatever you make will be deployable to the cloud.
{
# This description helps others understand the purpose of this Flake
description = "A flake that reports the OS using separate scripts with optional CUDA support and unfree packages allowed.";
# Inputs are the dependencies for our Flake
# They're pinned to specific versions to ensure reproducibility
inputs = {
# nixpkgs is the main repository of Nix packages
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
# flake-utils provides helpful functions for working with Flakes
flake-utils.url = "github:numtide/flake-utils";
};
# Outputs define what our Flake produces
# In this case, it's a development shell that works across different systems
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
# We're creating a custom instance of nixpkgs
# This allows us to enable unfree packages like CUDA
pkgs = import nixpkgs {
inherit system;
config = {
allowUnfree = true; # This is necessary for CUDA support
};
};
# These helpers let us adjust our setup based on the OS
isDarwin = pkgs.stdenv.isDarwin;
isLinux = pkgs.stdenv.isLinux;
# Common packages that we want available in our environment
# regardless of the operating system
commonPackages = with pkgs; [
python313Full # Python 3.13 interpreter
python313Packages.virtualenv # Tool to create isolated Python environments
figlet # For creating ASCII art welcome messages
tmux # Terminal multiplexer for managing sessions
zlib # Compression library for data compression
git # Version control system for tracking changes
curl # Command-line tool for transferring data with URLs
wget # Utility for non-interactive download of files from the web
cmake # Cross-platform build system generator
htop # Interactive process viewer for Unix systems
] ++ (with pkgs; pkgs.lib.optionals isLinux [
gcc # GNU Compiler Collection for compiling C/C++ code
stdenv.cc.cc.lib # Standard C library for Linux systems
]);
# This script sets up our Python environment and project
runScript = pkgs.writeShellScriptBin "run-script" ''
#!/usr/bin/env bash
# Activate the virtual environment
source .venv/bin/activate
# Create a fancy welcome message
REPO_NAME=$(basename "$PWD")
PROPER_REPO_NAME=$(echo "$REPO_NAME" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
figlet "$PROPER_REPO_NAME"
echo "Welcome to the $PROPER_REPO_NAME development environment on ${system}!"
echo
# Install Python packages from requirements.txt
# This allows flexibility to use the latest PyPI packages
# Note: This makes the environment less deterministic
echo "- Installing pip packages..."
if pip install --upgrade pip --quiet && \
pip install -r requirements.txt --quiet; then
package_count=$(pip list --format=freeze | wc -l)
echo "- Done. $package_count pip packages installed."
else
echo "Warning: An error occurred during pip setup."
fi
# Check if numpy is properly installed
if python -c "import numpy" 2>/dev/null; then
echo "- numpy is importable (good to go!)"
echo
echo "To start JupyterLab, type: start"
echo "To stop JupyterLab, type: stop"
echo
else
echo "Error: numpy could not be imported. Check your installation."
fi
# Create convenience scripts for managing JupyterLab
# Note: We've disabled token and password for easier access, especially in WSL environments
cat << EOF > .venv/bin/start
#!/bin/sh
echo "A JupyterLab tab will open in your default browser."
tmux kill-session -t jupyter 2>/dev/null || echo "No tmux session named 'jupyter' is running."
tmux new-session -d -s jupyter 'source .venv/bin/activate && jupyter lab --NotebookApp.token="" --NotebookApp.password="" --NotebookApp.disable_check_xsrf=True'
echo "If no tab opens, visit http://localhost:8888"
echo "To view JupyterLab server: tmux attach -t jupyter"
echo "To stop JupyterLab server: stop"
EOF
chmod +x .venv/bin/start
cat << EOF > .venv/bin/stop
#!/bin/sh
echo "Stopping tmux session 'jupyter'..."
tmux kill-session -t jupyter 2>/dev/null || echo "No tmux session named 'jupyter' is running."
echo "The tmux session 'jupyter' has been stopped."
EOF
chmod +x .venv/bin/stop
'';
# Define the development shell for Linux systems (including WSL)
linuxDevShell = pkgs.mkShell {
# Include common packages and conditionally add CUDA if available
buildInputs = commonPackages ++ (with pkgs; pkgs.lib.optionals (builtins.pathExists "/usr/bin/nvidia-smi") cudaPackages);
shellHook = ''
# Set up the Python virtual environment
test -d .venv || ${pkgs.python311}/bin/python -m venv .venv
export VIRTUAL_ENV="$(pwd)/.venv"
export PATH="$VIRTUAL_ENV/bin:$PATH"
# Customize the prompt to show we're in a Nix environment
# export PS1='$(printf "\033[01;34m(nix) \033[00m\033[01;32m[%s@%s:%s]$\033[00m " "\u" "\h" "\w")'
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath commonPackages}:$LD_LIBRARY_PATH
# Set up CUDA if available
if command -v nvidia-smi &> /dev/null; then
echo "CUDA hardware detected."
export CUDA_HOME=${pkgs.cudatoolkit}
export PATH=$CUDA_HOME/bin:$PATH
export LD_LIBRARY_PATH=$CUDA_HOME/lib64:$LD_LIBRARY_PATH
else
echo "No CUDA hardware detected."
fi
# Run our setup script
${runScript}/bin/run-script
'';
};
# Define the development shell for macOS systems
darwinDevShell = pkgs.mkShell {
buildInputs = commonPackages;
shellHook = ''
# Set up the Python virtual environment
test -d .venv || ${pkgs.python311}/bin/python -m venv .venv
export VIRTUAL_ENV="$(pwd)/.venv"
export PATH="$VIRTUAL_ENV/bin:$PATH"
# Customize the prompt to show we're in a Nix environment
# export PS1='$(printf "\033[01;34m(nix) \033[00m\033[01;32m[%s@%s:%s]$\033[00m " "\u" "\h" "\w")'
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath commonPackages}:$LD_LIBRARY_PATH
# Run our setup script
${runScript}/bin/run-script
'';
};
in {
# Choose the appropriate development shell based on the OS
devShell = if isLinux then linuxDevShell else darwinDevShell; # Ensure multi-OS support
});
}
Modernizing Our Nix Flake with UV
To make this Flake modern and efficient, I’ve replaced virtualenv
and pip
with uv
, a faster and more integrated Python dependency manager. The changes include:
- Swapping
python313Packages.virtualenv
withpkgs.uv
in the package list. - Using
uv venv
instead ofpython -m venv
to create the virtual environment in theshellHook
. - Replacing
pip install
withuv pip install
in therunScript
to handlerequirements.txt
and upgradepip
(thoughuv
manages this internally, I’ve kept the step for compatibility).
Here’s the updated Nix Flake in its entirety:
# ____ _ _ .--. ___________
# | _ \ __ _ _ ____ _(_)_ __ (_)_ __ ,--./,-. |o_o | | | |
# | | | |/ _` | '__\ \ /\ / / | '_ \| \ \/ / / # \ |:_/ | | | |
# | |_| | (_| | | \ V V /| | | | | |> < | | // \ \ |_____|_____|
# |____/ \__,_|_| \_/\_/ |_|_| |_|_/_/\_\ \ / (| | ) | | |
# `._,._,' /'\_ _/`\ | | |
# Solving the "Not on my machine" problem well. \___)=(___/ |_____|_____|
# Most modern development is done on Linux, but Macs are Unix. If you think Homebrew and Docker
# are the solution, you're wrong. Welcome to the world of Nix Flakes! This file defines a complete,
# reproducible development environment. It's like a recipe for your perfect workspace, ensuring
# everyone on your team has the exact same setup, every time. As a bonus, you can use Nix flakes on
# Windows under WSL. Plus, whatever you make will be deployable to the cloud.
{
# This description helps others understand the purpose of this Flake
description = "A flake that reports the OS using separate scripts with optional CUDA support and unfree packages allowed, now using uv for Python dependencies.";
# Inputs are the dependencies for our Flake
# They're pinned to specific versions to ensure reproducibility
inputs = {
# nixpkgs is the main repository of Nix packages
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
# flake-utils provides helpful functions for working with Flakes
flake-utils.url = "github:numtide/flake-utils";
};
# Outputs define what our Flake produces
# In this case, it's a development shell that works across different systems
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
# We're creating a custom instance of nixpkgs
# This allows us to enable unfree packages like CUDA
pkgs = import nixpkgs {
inherit system;
config = {
allowUnfree = true; # This is necessary for CUDA support
};
};
# These helpers let us adjust our setup based on the OS
isDarwin = pkgs.stdenv.isDarwin;
isLinux = pkgs.stdenv.isLinux;
# Common packages that we want available in our environment
# regardless of the operating system
commonPackages = with pkgs; [
python313Full # Python 3.13 interpreter
uv # Modern Python dependency manager replacing virtualenv and pip
figlet # For creating ASCII art welcome messages
tmux # Terminal multiplexer for managing sessions
zlib # Compression library for data compression
git # Version control system for tracking changes
curl # Command-line tool for transferring data with URLs
wget # Utility for non-interactive download of files from the web
cmake # Cross-platform build system generator
htop # Interactive process viewer for Unix systems
] ++ (with pkgs; pkgs.lib.optionals isLinux [
gcc # GNU Compiler Collection for compiling C/C++ code
stdenv.cc.cc.lib # Standard C library for Linux systems
]);
# This script sets up our Python environment and project
runScript = pkgs.writeShellScriptBin "run-script" ''
#!/usr/bin/env bash
# Activate the virtual environment
source .venv/bin/activate
# Create a fancy welcome message
REPO_NAME=$(basename "$PWD")
PROPER_REPO_NAME=$(echo "$REPO_NAME" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
figlet "$PROPER_REPO_NAME"
echo "Welcome to the $PROPER_REPO_NAME development environment on ${system}!"
echo
# Install Python packages from requirements.txt using uv
# This allows flexibility to use the latest PyPI packages
# Note: This makes the environment less deterministic
echo "- Installing pip packages with uv..."
if uv pip install --upgrade pip --quiet && \
uv pip install -r requirements.txt --quiet; then
package_count=$(pip list --format=freeze | wc -l)
echo "- Done. $package_count pip packages installed."
else
echo "Warning: An error occurred during uv pip setup."
fi
# Check if numpy is properly installed
if python -c "import numpy" 2>/dev/null; then
echo "- numpy is importable (good to go!)"
echo
echo "To start JupyterLab, type: start"
echo "To stop JupyterLab, type: stop"
echo
else
echo "Error: numpy could not be imported. Check your installation."
fi
# Create convenience scripts for managing JupyterLab
# Note: We've disabled token and password for easier access, especially in WSL environments
cat << EOF > .venv/bin/start
#!/bin/sh
echo "A JupyterLab tab will open in your default browser."
tmux kill-session -t jupyter 2>/dev/null || echo "No tmux session named 'jupyter' is running."
tmux new-session -d -s jupyter 'source .venv/bin/activate && jupyter lab --NotebookApp.token="" --NotebookApp.password="" --NotebookApp.disable_check_xsrf=True'
echo "If no tab opens, visit http://localhost:8888"
echo "To view JupyterLab server: tmux attach -t jupyter"
echo "To stop JupyterLab server: stop"
EOF
chmod +x .venv/bin/start
cat << EOF > .venv/bin/stop
#!/bin/sh
echo "Stopping tmux session 'jupyter'..."
tmux kill-session -t jupyter 2>/dev/null || echo "No tmux session named 'jupyter' is running."
echo "The tmux session 'jupyter' has been stopped."
EOF
chmod +x .venv/bin/stop
'';
# Define the development shell for Linux systems (including WSL)
linuxDevShell = pkgs.mkShell {
# Include common packages and conditionally add CUDA if available
buildInputs = commonPackages ++ (with pkgs; pkgs.lib.optionals (builtins.pathExists "/usr/bin/nvidia-smi") cudaPackages);
shellHook = ''
# Set up the Python virtual environment with uv
test -d .venv || ${pkgs.uv}/bin/uv venv .venv
export VIRTUAL_ENV="$(pwd)/.venv"
export PATH="$VIRTUAL_ENV/bin:$PATH"
# Customize the prompt to show we're in a Nix environment
# export PS1='$(printf "\033[01;34m(nix) \033[00m\033[01;32m[%s@%s:%s]$\033[00m " "\u" "\h" "\w")'
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath commonPackages}:$LD_LIBRARY_PATH
# Set up CUDA if available
if command -v nvidia-smi &> /dev/null; then
echo "CUDA hardware detected."
export CUDA_HOME=${pkgs.cudatoolkit}
export PATH=$CUDA_HOME/bin:$PATH
export LD_LIBRARY_PATH=$CUDA_HOME/lib64:$LD_LIBRARY_PATH
else
echo "No CUDA hardware detected."
fi
# Run our setup script
${runScript}/bin/run-script
'';
};
# Define the development shell for macOS systems
darwinDevShell = pkgs.mkShell {
buildInputs = commonPackages;
shellHook = ''
# Set up the Python virtual environment with uv
test -d .venv || ${pkgs.uv}/bin/uv venv .venv
export VIRTUAL_ENV="$(pwd)/.venv"
export PATH="$VIRTUAL_ENV/bin:$PATH"
# Customize the prompt to show we're in a Nix environment
# export PS1='$(printf "\033[01;34m(nix) \033[00m\033[01;32m[%s@%s:%s]$\033[00m " "\u" "\h" "\w")'
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath commonPackages}:$LD_LIBRARY_PATH
# Run our setup script
${runScript}/bin/run-script
'';
};
in {
# Choose the appropriate development shell based on the OS
devShell = if isLinux then linuxDevShell else darwinDevShell; # Ensure multi-OS support
});
}
For context, here’s the requirements.txt
this Flake works with:
httpx
jupyter-ai[all]
jupyterlab
nbstripout
numpy
pandas
requests
Why uv
Works Here
- Speed:
uv
is significantly faster thanvirtualenv
andpip
, making environment setup and dependency installation quicker. - Integration: It combines virtual environment creation (
uv venv
) and package management (uv pip install
) into a single tool, simplifying the Flake. - Compatibility: The
requirements.txt
file you provided (with packages likenumpy
,pandas
, andjupyterlab
) is fully supported byuv
, and the virtual environment it creates is compatible with the existing activation logic (source .venv/bin/activate
).
Testing It Out
To use this updated Flake:
- Save it as
flake.nix
in your project directory alongsiderequirements.txt
. - Run
nix develop
to enter the development shell. - The
run-script
will automatically set up the environment and install the dependencies. - Type
start
to launch JupyterLab, andstop
to shut it down.
This Flake now leverages uv
for a modern, efficient setup, aligning with the
trends I’ve noticed in projects like MCP and OpenManus. Next step? Maybe
tackling that agentic AI platform with this as a base—stay tuned!
Testing nv
Under Nix
Alright, so I simply copy over the requirements.txt
from another similar
project where I pioneered my multi-OS portable python nix flake and drop the new
flake.nix
file that uses uv
(courtesy of Grok 3, I should credit) into the
new repo, and nix develop
from there and start
JupyterLab…
[mike@nixos:~/repos/python_nix_flake]$ cp ../darwinix/requirements.txt .
[mike@nixos:~/repos/python_nix_flake]$ vim flake.nix
[mike@nixos:~/repos/python_nix_flake]$ nix develop
warning: creating lock file '/home/mike/repos/python_nix_flake/flake.lock':
• Added input 'flake-utils':
'github:numtide/flake-utils/[long hash]' (2024-11-13)
• Added input 'flake-utils/systems':
'github:nix-systems/default/[long hash]' (2023-04-09)
• Added input 'nixpkgs':
'github:NixOS/nixpkgs/[long hash]' (2025-03-09)
Using CPython 3.13.2 interpreter at: /nix/store/[long hash]-python3-3.13.2/bin/python
Creating virtual environment at: .venv
Activate with: source .venv/bin/activate
CUDA hardware detected.
# ____ _ _ _ __ _ _
# | _ \ _ _| |_| |__ ___ _ __ _ __ (_)_ __ / _| | __ _| | _____
# | |_) | | | | __| '_ \ / _ \| '_ \ | '_ \| \ \/ / | |_| |/ _` | |/ / _ \
# | __/| |_| | |_| | | | (_) | | | | | | | | |> < | _| | (_| | < __/
# |_| \__, |\__|_| |_|\___/|_| |_|___|_| |_|_/_/\_\___|_| |_|\__,_|_|\_\___|
# |___/ |_____| |_____|
Welcome to the Python_nix_flake development environment on x86_64-linux!
- Installing pip packages with uv...
- Done. 220 pip packages installed.
- numpy is importable (good to go!)
To start JupyterLab, type: start
To stop JupyterLab, type: stop
[mike@nixos:~/repos/python_nix_flake]$ start
A JupyterLab tab will open in your default browser.
If no tab opens, visit http://localhost:8888
To view JupyterLab server: tmux attach -t jupyter
To stop JupyterLab server: stop
[mike@nixos:~/repos/python_nix_flake]
Success! JupyterLab virtualenv
Created In Nix With nv
And wow! It worked like a charm! I have JupyterLab running in my OS’s default browser from a Python Nix Flake using
nv
!
Okay, so let me release this into the wild…
[mike@nixos:~/repos/python_nix_flake]$ git init
Initialized empty Git repository in /home/mike/repos/python_nix_flake/.git/
[mike@nixos:~/repos/python_nix_flake]$ git add flake.nix
[mike@nixos:~/repos/python_nix_flake]$ git add requirements.txt
[mike@nixos:~/repos/python_nix_flake]$ echo "flake.lock" > .gitignore
[mike@nixos:~/repos/python_nix_flake]$ echo ".venv" >> .gitignore
[mike@nixos:~/repos/python_nix_flake]$ git commit -am "Python Nix Flake with nv venv first commit"
[main (root-commit) f5faaa8] Python Nix Flake with nv venv first commit
2 files changed, 182 insertions(+)
create mode 100644 flake.nix
create mode 100644 requirements.txt
[mike@nixos:~/repos/python_nix_flake]$ git remote add origin git@github.com:miklevin/python_nix_flake.git
[mike@nixos:~/repos/python_nix_flake]$ git branch -M main
[mike@nixos:~/repos/python_nix_flake]$ git push -u origin main
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 48 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 3.29 KiB | 3.29 MiB/s, done.
Total 4 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To github.com:miklevin/python_nix_flake.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.
[mike@nixos:~/repos/python_nix_flake]$
Gemini’s Take
As usual, I have Gemini (currently 2.0 Advanced) summarize my articles with some alternative title and headline ideas, plus its opinion.
This article details the author’s journey to modernize his Python Nix Flake by replacing traditional pip
and virtualenv
with the faster uv
tool, driven by experiences with AI platforms like MCP and OpenManus. The author provides a step-by-step guide on updating the Nix Flake configuration, highlighting the efficiency and integration benefits of uv
in managing Python dependencies across different operating systems, culminating in a successful deployment of JupyterLab within a Nix environment.
Ideas for Titles and Headlines:
- Titles:
- “Modernizing Python Nix Flakes with UV: A Speed Boost for Dependency Management”
- “From Pip to UV: Streamlining Python Environments in Nix”
- “Nix and UV: Building Faster Python Development Environments”
- “UV vs Pip in Nix: A Practical Migration Guide”
- Headlines:
- “Say Goodbye to Slow Installs: UV Revolutionizes Python Nix Flakes”
- “AI Platforms Demand Speed: Why UV Is the Future of Python Dependency Management”
- “One Tool to Rule Them All: UV Simplifies Virtual Environments in Nix”
- “JupyterLab in Nix, Powered by UV: A Step-by-Step Success Story”
AI’s Opinion of the Article:
This article is a valuable, practical guide for developers looking to optimize their Python development environments using Nix. The author’s candid account of his initial struggles and subsequent success with uv
makes the information relatable and actionable. The step-by-step code examples and clear explanations of the changes make it easy for readers to follow along and implement the solutions in their own projects. The author’s connection of the changes to emerging AI platforms also provides relevant context, highlighting the importance of efficient dependency management in modern software development. The inclusion of the actual terminal output of the successful test is a great touch, and adds to the validity of the article.