--- title: Replacing `pip` In Python Nix Flake With `uv` permalink: /futureproof/python-nix-flake-uv/ description: I decided to ditch the old `pip` and `virtualenv` combo in my Python Nix Flake and embrace `uv`, the new hotness in dependency management, after seeing it pop up in AI projects like MCP and OpenManus. This article walks you through my journey of updating the Nix Flake, showing you how to streamline your Python environments for speed and efficiency, and ultimately getting JupyterLab up and running like a charm. It's all about keeping up with the latest tools and making development smoother, especially when you're diving into cutting-edge AI stuff. meta_description: Modernize your Python Nix Flake by replacing pip/virtualenv with uv for faster dependency management and swift JupyterLab launches—ideal for AI projects. meta_keywords: Python Nix Flake, uv, dependency management, virtual environments, pip replacement, JupyterLab, cross-platform, nix flakes, modern Python tooling, AI development layout: post sort_order: 1 --- ## 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](https://github.com/miklevin/python_nix_flake). ### 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, ahaha! 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: ```nix # ____ _ _ .--. ___________ # | _ \ __ _ _ ____ _(_)_ __ (_)_ __ ,--./,-. |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` with `pkgs.uv` in the package list. - Using `uv venv` instead of `python -m venv` to create the virtual environment in the `shellHook`. - Replacing `pip install` with `uv pip install` in the `runScript` to handle `requirements.txt` and upgrade `pip` (though `uv` manages this internally, I’ve kept the step for compatibility). Here’s the updated Nix Flake in its entirety: ```nix # ____ _ _ .--. ___________ # | _ \ __ _ _ ____ _(_)_ __ (_)_ __ ,--./,-. |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: ```python httpx jupyter-ai[all] jupyterlab nbstripout numpy pandas requests ``` ### Why `uv` Works Here - **Speed**: `uv` is significantly faster than `virtualenv` and `pip`, 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 like `numpy`, `pandas`, and `jupyterlab`) is fully supported by `uv`, 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: 1. Save it as `flake.nix` in your project directory alongside `requirements.txt`. 2. Run `nix develop` to enter the development shell. 3. The `run-script` will automatically set up the environment and install the dependencies. 4. Type `start` to launch JupyterLab, and `stop` 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... ```bash [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... ```bash [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.