If you have installed Python and worked on a few projects, your usual workflow probably looks something like this. Install Python, make sure pip and venv are available, create a virtual environment for each project, and use pip to install packages inside it. That is how we have been doing things throughout this series, and it works. It is what most of the Python world has been doing for years.
uv is a relatively new tool that bundles all of this into one command. It manages Python version installs, virtual environments, package installs, and dependency locking, all in a single binary. It is written in Rust, which makes it fast (like really fast). Most operations that take pip several seconds finish in milliseconds with uv.
In this part, we will start from a completely fresh Ubuntu machine (24.04) that only has the system Python installed. As you will see in a moment, we do not have pip or venv available yet, and normally we would need to install them separately before we could do anything.
A Fresh Ubuntu VM
Here is what is happening on the fresh Ubuntu VM. python --version fails because Ubuntu does not ship the python alias by default, you have to use python3 explicitly. python3 --version works and confirms Python 3.12.3 is installed as the system Python.
suresh@test-vm:~$ python --version
Command 'python' not found, did you mean:
command 'python3' from deb python3
command 'python' from deb python-is-python3
suresh@test-vm:~$ python3 --version
Python 3.12.3
Both pip and pip3 are missing, and so is the venv module, python3 -m venv venv fails and tells us to install python3.12-venv first.
suresh@test-vm:~$ pip --version
Command 'pip' not found, but can be installed with:
sudo apt install python3-pip
suresh@test-vm:~$ pip3 --version
Command 'pip3' not found, but can be installed with:
sudo apt install python3-pip
suresh@test-vm:~$ python3 -m venv venv
The virtual environment was not created successfully because ensurepip is not
available. On Debian/Ubuntu systems, you need to install the python3-venv
package using the following command.
apt install python3.12-venv
You may need to use sudo with that command. After installing the python3-venv
package, recreate your virtual environment.
Failing command: /home/suresh/venv/bin/python3
So on a stock Ubuntu system, we have Python itself but neither of the two things we normally need, pip and venv. Under the traditional workflow, we would install both of these before doing anything else.
We will install uv instead, create a new project pinned to a specific Python version, install a package, and run our first script. By the end, you will have a working modern Python setup that is faster and cleaner than the traditional pip/venv workflow.
Installing uv
uv comes as a single binary, so you do not need to install anything Python-related first. On macOS and Linux, install it with the following one-liner.
curl -LsSf https://astral.sh/uv/install.sh | sh
On Windows, use the PowerShell installer from the uv documentation. Once installed, close and reopen your terminal so uv is on your PATH, then verify with uv --version.
uv --version
uv 0.11.28 (x86_64-unknown-linux-gnu)
Creating a New Project
Now for the fun part. Instead of manually creating a folder, installing Python, setting up a virtual environment, and installing pip, uv can do all of it in one command. For this example, let’s pretend our shiny new project needs Python 3.13.14.
uv init my_project --python 3.13.14
Output
Initialized project `my-project` at `/home/suresh/my_project`
This creates a new project folder called my_project, pins it to Python 3.13.14, and sets up a proper project structure inside. Let's take a look.
cd my_project
ls
Output
main.py pyproject.toml README.md
main.py is a placeholder script for you to start with, pyproject.toml is your project's config file (which we will look at in a moment), and README.md is an empty README for your project.
If you run ls -la you will also see a .git folder, a .gitignore file, and a .python-version file that contains 3.13.14. uv sets up Git for you, ignores the right files, and pins the Python version, all automatically.
If you already have a folder and just want to turn it into a uv project, cd into it first and run uv init without a project name.
cd other_project
uv init --python 3.13.14
Output
Initialized project `other-project`
Installing the Pinned Python Version
Notice that uv init only pinned the Python version, it did not actually install it yet. To install it, run the following inside the project folder.
uv python install
Output
Installed Python 3.13.14 in 839ms
+ cpython-3.13.14-linux-x86_64-gnu (python3.13)
Under a second. Compare that to downloading Python from the website or building it from source, this is a huge quality-of-life improvement. uv reads .python-version in the current folder and installs exactly that version.
Adding Packages
Adding a package to your project uses the uv add command. Let's install Netmiko.
uv add netmiko
Output
Using CPython 3.13.14
Creating virtual environment at: .venv
Resolved 19 packages in 125ms
Prepared 18 packages in 203ms
Installed 18 packages in 13ms
+ bcrypt==5.0.0
+ cffi==2.1.0
+ cryptography==49.0.0
+ invoke==3.0.3
+ netmiko==4.7.0
+ paramiko==4.0.0
...
A lot happened there in one command. uv created a virtual environment automatically (in a folder called .venv), resolved all the dependencies Netmiko needs, and installed everything. Traditionally, we would have run python3 -m venv venv to create the virtual environment, activated it with source venv/bin/activate, and only then run pip install netmiko.
If you look at pyproject.toml now, Netmiko has been added as a dependency. This is how modern Python projects track what they depend on, in a file that gets committed to Git alongside your code.
[project]
name = "my-project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13.14"
dependencies = [
"netmiko>=4.7.0",
]
Activating the Virtual Environment
You can still activate the virtual environment the traditional way if you want to run scripts or check versions.
suresh@test-vm:~/my_project$ source .venv/bin/activate
(my-project) suresh@test-vm:~/my_project$
(my-project) suresh@test-vm:~/my_project$ python --version
Output
Python 3.13.14
Notice python now works (not just python3), because uv sets up the environment properly. The (my-project) prefix in your prompt also confirms you are inside the project's virtual environment.
From here on, everything works the same as it did with regular pip and venv. You can write scripts in main.py (or add new files), run them with python main.py, and use any package installed in the venv. The difference is that uv handled all the setup for you in a fraction of the time.
Sharing the Project
The real payoff comes when you want to share this project with someone else, or move it to another machine. Because uv stores the Python version in .python-version and the dependencies in pyproject.toml and uv.lock, anyone who clones the project can recreate the exact same environment with a single command.
Say you push this project to GitHub. Someone clones it on their machine.
git clone https://github.com/yourname/my_project.git
cd my_project
Now they run one command, yes just a single command.
uv sync
That is it. uv sync reads .python-version, pyproject.toml, and uv.lock, then installs the exact Python version, creates a virtual environment, and installs every dependency at the exact same version you had.
Compare this to the traditional workflow. Someone would need to have the right Python version already installed, create a virtual environment themselves, activate it, then run pip install -r requirements.txt. And even then, unless you were careful about pinning versions in the requirements file, they might end up with a slightly different set of packages than you had.
The uv.lock file is the important piece here. It records the exact version of every package and every transitive dependency, so the environment is fully reproducible. As long as it lives alongside pyproject.toml in Git, anyone can rebuild your project's environment exactly.
uv also lets you run scripts without activating the virtual environment at all. Instead of source .venv/bin/activate followed by python main.py, you can just do this.
uv run main.py
uv figures out the virtual environment for the current project, activates it under the hood for that command and runs your script.
You can still activate the venv the normal way if you prefer (I still do it out of habit), but uv run is worth knowing about, especially in automated setups where activating a venv is awkward.
Removing Packages
Removing a package is the counterpart to adding one, and it works exactly how you would expect.
uv remove netmiko
Output
(my-project) suresh@test-vm:~/my_project$ uv remove netmiko
Resolved 1 package in 1ms
Uninstalled 18 packages in 32ms
- bcrypt==5.0.0
- cffi==2.1.0
- cryptography==49.0.0
- invoke==3.0.3
- markdown-it-py==4.2.0
- mdurl==0.1.2
- netmiko==4.7.0
- ntc-templates==9.2.0
- paramiko==4.0.0
- pycparser==3.0
- pygments==2.20.0
- pynacl==1.6.2
- pyserial==3.5
- pyyaml==6.0.3
- rich==15.0.0
- ruamel-yaml==0.19.1
- scp==0.15.0
- textfsm==2.1.0
uv removes Netmiko, uninstalls anything that was only there because Netmiko needed it, and updates both pyproject.toml and uv.lock so your project reflects the change.
Seeing What is Installed
To see what is currently in your project's environment, use uv tree.
uv tree
Output
(my-project) suresh@test-vm:~/my_project$ uv tree
Resolved 19 packages in 0.60ms
my-project v0.1.0
└── netmiko v4.7.0
├── ntc-templates v9.2.0
│ └── textfsm v2.1.0
├── paramiko v4.0.0
│ ├── bcrypt v5.0.0
│ ├── cryptography v49.0.0
│ │ └── cffi v2.1.0
│ │ └── pycparser v3.0
│ ├── invoke v3.0.3
│ └── pynacl v1.6.2
│ └── cffi v2.1.0 (*)
├── pyserial v3.5
├── pyyaml v6.0.3
├── rich v15.0.0
│ ├── markdown-it-py v4.2.0
│ │ └── mdurl v0.1.2
│ └── pygments v2.20.0
├── ruamel-yaml v0.19.1
├── scp v0.15.0
│ └── paramiko v4.0.0 (*)
└── textfsm v2.1.0
(*) Package tree already displayed
This is much nicer than pip list, which just shows a flat list of every package. uv tree shows the actual dependency structure, so you can see that Netmiko is your only direct dependency, and everything else is there because Netmiko needs it (or one of its dependencies does).
The (*) next to some entries means "already shown above." For example, cffi appears under both paramiko and cryptography, but it is only expanded the first time it appears to keep the output readable.
If you prefer a flat list like the traditional pip list, you can use uv pip list instead.
(my-project) suresh@test-vm:~/my_project$ uv pip list
Package Version
-------------- -------
bcrypt 5.0.0
cffi 2.1.0
cryptography 49.0.0
invoke 3.0.3
markdown-it-py 4.2.0
mdurl 0.1.2
netmiko 4.7.0
ntc-templates 9.2.0
paramiko 4.0.0
pycparser 3.0
pygments 2.20.0
pynacl 1.6.2
pyserial 3.5
pyyaml 6.0.3
rich 15.0.0
ruamel-yaml 0.19.1
scp 0.15.0
textfsm 2.1.0
Running One-Off Scripts with Dependencies
Sometimes you want to write a quick script that needs a package, but you do not want to add it to a project. Maybe it is a throwaway test, or a small utility you just want to run once. uv has a nice option for this called --with, which lets you install a package just for that one command.
Say you have a script called check_device.py that uses Netmiko, but you have not set up a project or added Netmiko as a dependency.
uv run --with netmiko check_device.py
uv creates a temporary environment on the fly, installs Netmiko into it, runs your script, and then throws the environment away when the script finishes.
This is really useful for one-off scripts, quick tests, or utility snippets you find online. Instead of adding Netmiko to a project just to try something, you can run it in isolation.
You can also chain multiple packages with --with.
uv run --with netmiko --with rich check_device.py
Using uv as a Drop-in for pip
Not every project you work with will have been set up using uv. You might inherit a project that uses the traditional requirements.txt approach, and you do not want to rewrite the whole thing just to use uv. Good news, uv has a pip subcommand that lets you use it as a drop-in replacement for pip, with the same commands you already know.
Say you have a project with a requirements.txt file that looks like this.
bcrypt==5.0.0
cffi==2.1.0
cryptography==49.0.0
invoke==3.0.3
markdown-it-py==4.2.0
mdurl==0.1.2
netmiko==4.7.0
ntc_templates==9.2.0
paramiko==4.0.0
pycparser==3.0
Pygments==2.20.0
PyNaCl==1.6.2
pyserial==3.5
PyYAML==6.0.3
rich==15.0.0
ruamel.yaml==0.19.1
scp==0.15.0
textfsm==2.1.0
Normally you would create a virtual environment, activate it, and run pip install -r requirements.txt. With uv, you can do the same, just faster.
uv venv
source .venv/bin/activate
uv pip install -r requirements.txt
suresh@test-vm:~/my_project/old_project$ uv venv
Using CPython 3.13.14
Creating virtual environment at: .venv
Activate with: source .venv/bin/activate
suresh@test-vm:~/my_project/old_project$ source .venv/bin/activate
(old_project) suresh@test-vm:~/my_project/old_project$
(old_project) suresh@test-vm:~/my_project/old_project$
(old_project) suresh@test-vm:~/my_project/old_project$ uv pip install -r requirements.txt
Resolved 18 packages in 55ms
Installed 18 packages in 10ms
+ bcrypt==5.0.0
+ cffi==2.1.0
+ cryptography==49.0.0
+ invoke==3.0.3
+ markdown-it-py==4.2.0
+ mdurl==0.1.2
+ netmiko==4.7.0
+ ntc-templates==9.2.0
+ paramiko==4.0.0
+ pycparser==3.0
+ pygments==2.20.0
+ pynacl==1.6.2
+ pyserial==3.5
+ pyyaml==6.0.3
+ rich==15.0.0
+ ruamel-yaml==0.19.1
+ scp==0.15.0
+ textfsm==2.1.0
uv venv creates a virtual environment (equivalent to python3 -m venv .venv), and uv pip install behaves the same as regular pip install, but uses uv's fast resolver and installer under the hood.
This is a good middle ground when you are not ready to fully commit to uv init and pyproject.toml. You still get the speed benefits of uv without changing how the project is structured.


