An image describing whether to go for uv or venv for package management

Introduction to uv with a Python Project

Preface

Python developers often rely on virtual environments and package managers to keep projects organized and reproducible. A recent new tool called uv has emerged as an alternative to traditional solutions like venv, virtualenv, and poetry. The earliest package release can be dated back to Feb 2024 and now it already has over 210 releases and 66.3K stars! In this post, we’ll explore uv, its pros and cons, and demonstrate a step-by-step Python project setup as an example.

Why Use uv?

Managing dependencies across multiple Python projects can be cumbersome. Traditional tools like venv require manual activation, while poetry or pipenv have more features but can be heavy or opinionated. uv aims to simplify project setup, virtual environment management, and reproducible environments in a lightweight, user-friendly way.

Pros

  • Lightweight and fast
  • Easy creation and management of project-specific virtual environments
  • Supports multiple Python versions easily
  • Built-in seeding mechanism for reproducible environments
package installation speed comparison
Speed comparison for package installation

As for the cons, actually there are currently none that I can think of. One article from Bite Code, “A year of uv: pros, cons, and should you migrate (Feb 2025)”, has concluded the pros and cons of uv well, and it also has nothing critical to say about the apparent disadvantages of using uv. The only suggestion will be that always try uv first in the project, and only if that fails we turn back to the original way of managing the dependency in the project.

Step-by-Step Example: Python Project with uv

Let’s create a small Python project using uv. The demo has also been uploaded to my GitHub called myuvdemo repo.

Step 1: Intall uv

There are many ways of installing uv, for example, via curl, wget, brew, winget, scoop, pipx, or docker. For more details, please refer to the official documentation Installing uv.

Step 2: Create a GitHub repo.

Go to your GitHub and create a new repository, then copy the repo address and git clone it to your local machine. For my example, I have created a repo called myuvdemo via GitHub. Afterwards, I clone it to my computer via the following command:

github_repo_create_myuvdemo
Bash
git clone https://github.com/wkCircle/myuvdemo.git

Step 3: Initialize uv within the project scope

Change the current directory (cd) into the repository in the terminal and initialize uv with the desired python version.

Bash
cd myuvdemo
uv init --python 3.12

Afterwards, we will see that 4 additional files have been added to the repo directory: README.md, main.py, pyproject.toml, and.python-version. We can check the list of files including the hidden ones under the current directory via ls -a command in bash terminal.

  • README.md: an empty file that is used to provide repo documentation.
  • main.py: the main python script file but currently contains only the template code with a print function.
  • .python-version: a hidden file that tells uv the default Python version for the current repo. To check this, we can use the command cat .python-version to print out the file content, in my case it is “3.12”. This file doesn’t need to be uploaded to GitHub and can be put into .gitignore.
  • pyproject.toml: the most important file that uv uses to describe the current project, python version, and to mange package dependencies in the first place.

pyproject.toml initially contains the following information of key-value pairs format:

TOML
[project]
name = "myuvdemo"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []

The official uv website also describes what each file is doing, you can find more information here in Working on projects, uv.

Step 4: Add packages

Now if we want to add pandas, numpy, and matplotlib into the current repo environment, we can use the following command:

Bash
uv add pandas numpy matplotlib

uv will install these packages as well as dependent packages into a virtual environment folder .venv:

uv-add-packages-output

If we check the directory again using ls -a, then we see that .venv folder, uv.lock has been added, and actually pyproject.toml has also been updated.

  • .venv: is the dedicated virtual environment that contains the downloaded packages and the python executable file with the correct python version that we specify earlier.
  • uv.lock: is a file that stores information about all package dependencies of the virtual envrionment. It is cross-platform applicable and should not be modified manually. E.g., all 14 packages listed in the screenshot above will appear in the lock file.
  • pyproject.toml: the dependencies key will get updated by recording the main packages that user specify when executing the uv add command. In our example, the updated pyproject.toml will look like:
TOML
[project]
name = "myuvdemo"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "matplotlib>=3.10.6",
    "numpy>=2.3.2",
    "pandas>=2.3.2",
]

While both pyproject.toml and uv.lock record dependencies, pyproject.toml record only the direct packages that user explicitly specify to install and uv.lock includes all packages that are either the direct packages the user specified, or the indirect packages that support the direct packages. According to Real Python article Managing Python Projects With uv: An All-in-One Solution, these are referred to as direct dependencies and transitive dependencies respectively.

Similarly, to remove a package from existing venv, we can simply use uv remove command, e.g.:

Bash
uv remove numpy

Step 5: run python script

We modify the main.py with the following code as an example:

Python
# main.py
import pandas as pd
from matplotlib import pyplot as plt

def main():
    print("Hello from main function!")
    print("Creating a df...")
    df = pd.DataFrame({'idx': [1, 2, 3, 4, 5], 'y': [1, 1, 10, 20, 60]})
    print("Plotting the df")
    df.plot()
    plt.show()


if __name__ == "__main__":
    main()

And then use the following command to execute the script:

Bash
uv run main.py

Once executed, we will receive the following output and the generated figure:

output screenshot when executing uv run main.py
output df.plot() screenshot when executing uv run main.py

uv run command will automatically use the correct venv to run the script. If you use a different command other than using uv, e..g, python main.py, you might probably be using a different python coming somewhere else from your system and might end up having an error such as Module Not Found Error. It is best to always make sure using uv command to run the script so you won’t mistakenly use a wrong one.

Step 6: upgrade and/or remove packages

When packages are too old and we want to use the latest version, simply type the following command:

Bash
uv add --upgrade <package_name>

And when a package is no longer needed, we can remove them to keep a clean envrionment:

Bash
uv remove <package_name>

Short Summary

Through Step 1-6, we have built a dedicated virtual environment with package dependency management via uv and can execute the script successfully. Congrats! But there are more advanced usage of uv that helps to further maintain the environment!

Reproduce the same venv

When there are multiple developers who want to work on the same project, they also need to install uv and reproduce the same venv before running the python scripts. But how can they do that? Which file are needed for them to reproduce the same environment on their computers?

The answer is uv.lock and pyproject.toml these two files. As long as they are maintained are uploaded by GitHub, the other developers can simply use these two files to reproduce the same venv via the following command:

Bash
uv run <any of your .py file>

You will find that uv automatically installs the virtual environment in .venv/ folder if there is no existing one before executing the python script. When you run the uv run command again, there is no longer the venv installation since the current one already exist and uv will just directly execute the python script.

Alternatively, we can also use the following command:

Bash
uv lock
uv sync

the first line helps creating a lock file (if not existed) or updating it (if existed), and the second line reads pyproject.toml and uv.lock, then create the .venv/ folder (if not existed) or update it (if existed).

Manage packages in different dependency groups

Because different packages have different purpose to be installed in to the current project, we can manage them better by putting them into different dependency groups so that when it comes to testing stage or deployment stage of your project, you can choose to install only certain groups of them to keep a clean and maintainable environment everywhere.

For example, you wish to use develop a front-end beautiful website which provides users some chart analysis and insights. Then during the development phase you would likely to install not just the pandas, numpy, matplotlib etc to help you achieve your goal, but also tools such as pre-commit, pytest to help make your code robust and speed you up (by Continuous Integration, e.g., Setup CI with GitHub Actions for Flutter). However, during the production/deployment phase, you don’t need these testing tools but just the core packages. Therefore, having those packages being categorized into different dependency groups beforehand would help the partial installation at the deployment stage and thus reduce installation time, keep deployment environment clean etc.

To achieve the idea described above, we can, for example, put those packages into main dependency and those testing packages into dev dependency.

When we use uv add <package_name>, by default uv installs the package into main dependency:

Bash
# add core packages to main dependency
uv add pandas numpy matplotlib

When we want to add other packages into another group dependency, use --group argument:

Bash
# add testing packages to dev dependency
uv add --group dev pre-commit pytest

Afterwards, the pyproject.toml and uv.lock will be updated and contain the information that which package belongs to which group. For example, pyporject.toml will have the following content:

TOML
[project]
name = "myuvdemo"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "matplotlib>=3.10.6",
    "numpy>=2.3.2",
    "pandas>=2.3.2",
]

[dependency-groups]
dev = [
    "pre-commit>=4.3.0",
    "pytest>=8.4.2",
]

Now, when it comes to reproducing the same environment, pay attention that uv sync by default installs only the main and dev dependency packages.

Notice

To include extra groups, use extra command:

Bash
uv sync --extra <group_name>

Since dev group is a special one and is included in syncing by default, there are corresponding commands to handle it.

  • The --no-dev flag can be used to exclude the dev group.
  • The --only-dev flag can be used to install the dev group without the project and its dependencies.
  • For example, to include main and foo dependency groups but not the dev, one can use uv sync --no-dev --extra foo

Summary

uv is a modern, lightweight tool that simplifies Python project setup and dependency management. Unlike traditional solutions, it combines speed with reproducibility, making it easier to keep environments consistent across different machines. With commands like uv add, uv lock, and uv sync, you can quickly manage dependencies, organize them into groups, and share reproducible environments with collaborators. To run a python script, the command is also easy with uv run.

Whether you’re setting up a small side project or collaborating on a larger codebase, uv provides a clean and efficient workflow that reduces friction and helps keep your Python projects maintainable.

References

Chung
Chung

A data wrangler who treats models and code like fine art — equal parts logic and caffeine. I build hierarchical forecasting models for 50,000+ time series while secretly wondering if lumpy demand is just the universe trolling me. When I’m not deciphering data, I’m scaling bouldering walls or rewatching Big Bang Theory — solving puzzles, whether in code or on rock, is basically my love language.

Articles: 4

Leave a Reply

Your email address will not be published. Required fields are marked *