Back to Scaling Python Applications guides

A Deep Dive into UV: The Fast Python Package Manager

Stanley Ulili
Updated on February 17, 2025

uv is a next-generation package manager for Python that delivers exceptional speed and modern dependency management. It is designed to be a drop-in replacement for pip, pip-tools, and virtualenv, providing a significantly faster and more reliable experience.

This article will walk you through getting started with uv, demonstrating its key features, and showing how to optimize your Python development workflow with it.

Prerequisites

Before diving into uv, ensure you have a recent version of Python installed on your machine. However, one of uv's standout features is that it does not require Python to be pre-installed—it can download and manage Python versions for you.

Why use uv?

Before diving into uv, let's understand why it significantly improves over traditional Python package management. Here's what makes uv special:

  • Fast package management: While pip might take several seconds to install a package, uv typically completes installations in milliseconds
  • Automatic virtual environments: Instead of manually running python -m venv and remembering to activate environments, uv handles this automatically.

  • Modern dependency resolution: Unlike pip, which can sometimes produce inconsistent environments, uv guarantees reproducible installations through its precise dependency resolution and lockfile system.

  • Project-first approach: Rather than managing loose packages with requirements.txt, uv uses pyproject.toml - the official Python packaging standard - providing better organization and version control.

Think of uv as a power tool that combines the best aspects of pip, virtualenv, and pip-tools into a single, faster, more reliable solution.

Throughout this tutorial, you'll see how these features work together to simplify your Python development workflow.

Installing uv

In this section, you will install the uv package installer for Python.

While uv can install Python, it's often already installed on most systems. This tutorial assumes you have Python installed. To check if Python is already installed on your system, run:

 
python3 --version

You should see output similar to:

 
Python 3.13.2

The easiest way to install uv is by using the official installation script:

 
curl -LsSf https://astral.sh/uv/install.sh | sh

This will download and install uv to ~/.local/bin by default. Following that, restart your shell:

 
source $HOME/.local/bin/env

To verify that uv has been installed successfully, run:

 
uv --version
Output
uv 0.5.31 (e38ac4900 2025-02-12)

This output confirms that uv has been installed successfully and is ready to use.

Getting started with uv

Now that uv is installed, let's explore its core functionality. At its heart, uv is a fast package manager and virtual environment manager for Python. Unlike pip, it automatically creates and manages virtual environments, ensuring a clean and isolated development setup.

To get started, create a project directory and move into it:

 
mkdir uv-demo && cd uv-demo

With uv, you don’t need to manually create a virtual environment using venv. Instead, simply run:

 
uv venv
Output
Using CPython 3.13.2 interpreter at: /opt/homebrew/opt/python@3.13/bin/python3.13
Creating virtual environment at: .venv
Activate with: source .venv/bin/activate

This command does three things:

  • It creates a virtual environment in .venv inside your project directory
  • It automatically selects the best Python version** available
  • If the required Python version is missing, uv downloads and installs it

Next, activate the virtual environment:

 
source .venv/bin/activate

Once the virtual environment is ready, installing packages is incredibly fast. Instead of using pip, use:

 
uv pip install requests
Output

Resolved 5 packages in 1.60s
Prepared 5 packages in 2.55s
Installed 5 packages in 8ms
 + certifi==2025.1.31
 + charset-normalizer==3.4.1
 + idna==3.10
 + requests==2.32.3
 + urllib3==2.3.0

This is significantly faster than pip install because uv skips the traditional wheel-building step and installs packages in parallel.

You can also install multiple packages at once:

 
uv pip install requests numpy pandas

And if you need a specific version:

 
uv pip install 'Django>=5.0'

Once your packages are installed, you can run Python scripts within the uv-managed environment without activating it manually. Create a file named app.py:

app.py
import numpy as np

# Create a sample array
arr = np.array([1, 2, 3, 4, 5])

print(f"And here's the mean of the array: {np.mean(arr)}")

Run it using:

 
uv run python app.py
Output
And here's the mean of the array: 3.0

This automatically uses the correct virtual environment, even if you move between projects.

You've seen how dependency management with uv simplifies and optimizes Python development. If you're used to pip, this might feel familiar, but uv takes it a step further.

Instead of manually handling environments and installations, uv automates and accelerates the process while ensuring consistency.

Creating projects with uv

In the last section, you learned how to create virtual environments and install packages using uv. While this works, it's similar to traditional workflows with pip and venv.

Let's explore uv's more powerful project management features that make dependency handling more robust and reproducible.

Instead of installing packages ad hoc, modern Python development best practices recommend declaring dependencies in a project configuration file.

uv fully embraces this approach by using pyproject.toml - the official Python packaging standard replacing traditional requirements.txt files.

This provides several advantages:

  • Declares both project metadata and dependencies in one place
  • Supports precise version constraints and dependency groups
  • Enables reproducible builds across different machines
  • Integrates smoothly with modern Python packaging tools

Since we previously created a project directory, uv-demo, let's start fresh with a properly structured project. First, exit the previous virtual environment and directory:

 
deactivate
 
cd ..

Initialize a new project using uv:

 
uv init uv-managed-demo

This creates a directory uv-managed-demo. Next, move into it:

 
cd uv-managed-demo

uv generates the following essential files:

 
ls
Output
.
├── README.md        # A basic project readme
├── pyproject.toml   # Defines project metadata and dependencies
└── hello.py    # A sample script file

If you open pyproject.toml, you'll see a structure like this:

pyproject.toml
[project]
name = "uv-managed-demo"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = []

At this point, the project hasn’t defined any dependencies yet.

Now that we have a properly structured project, let’s move on to managing dependencies efficiently with uv.

Managing dependencies with uv

In the previous section, you explored how uv allows you to structure your projects using pyproject.toml, ensuring a clean and organized development environment.

This section will build upon that to show you how uv simplifies dependency management, making package installation, removal, and synchronization effortless.

uv allows you to declare dependencies in pyproject.toml using the uv add command. This not only installs the package but also updates your project configuration automatically.

For example, to add requests:

 
uv add requests

Once installed, pyproject.toml is updated to include:

pyproject.toml
...
dependencies = [
    "requests>=2.32.3",
]

Unlike traditional package installation methods, uv does more than just install a package—it locks the dependencies. When a package is installed, uv creates a lock file (uv.lock), which:

  • Captures exact package versions to ensure that dependencies remain consistent across different machines
  • Prevents version conflicts
  • Optimizes future installations, since reinstalling from a lock file is much faster since dependencies are pre-resolved

With uv, you can install multiple packages in a single command, making dependency management more efficient:

 
uv add numpy pandas

Or specify version constraints:

 
uv add "Django>=4.0"

This ensures that your project always installs a compatible version of Django, preventing accidental upgrades that could break functionality.

And if you no longer need a package, you can remove it easily:

 
uv remove requests

When you remove a package with uv, it not only deletes the package from the virtual environment but also updates pyproject.toml to reflect the change and modifies the lock file uv.lock to maintain consistency.

This ensures your project remains clean, organized, and free from unnecessary dependencies, keeping your environment lightweight and well-managed.

If you're working on a team or setting up your project on a new machine, you don’t have to install every package manually. You can clone a repository that already has a pyproject.toml and uv.lock, and install the exact package versions by running:

 
uv sync

This guarantees that all dependencies align with the locked versions, the environment remains fully up-to-date, and no unnecessary packages are installed. Unlike pip install -r requirements.txt, which can lead to inconsistencies, uv sync enforces strict version control, ensuring a stable and reproducible setup across all environments.

Since uv automatically manages the virtual environment, you don’t need to activate it manually. Instead, run scripts and commands directly:

 
uv run hello.py
Output
Hello from uv-managed-demo!

With this modern approach to dependency management, you can ensure consistent environments across development and deployment, eliminating the dreaded "works on my machine" issues.

Managing dependencies with groups in uv

In most Python projects, dependencies fall into different categories based on their purpose. Instead of treating all dependencies the same, uv provides dependency groups, allowing you to organize better and manage your project's packages.

Dependency groups allow you to separate dependencies based on their usage. This helps:

  • Keep production environments lightweight by installing only necessary dependencies
  • Organize tools more effectively, separating development tools from runtime dependencies
  • Improve collaboration by ensuring everyone on the team works with the same set of tools

By default, when you add a package using uv add, it is added to the main project dependencies under [project.dependencies] in pyproject.toml:

 
uv add fastapi

This results in:

pyproject.toml
...
[project]
dependencies = [
    "fastapi>=0.100.0",
]

However, not all dependencies belong in production. You can group dependencies based on their use case using --group.

development tools—such as testing frameworks and formatters—should not be bundled with production dependencies. To keep them separate, use the --dev flag:

 
uv add --dev pytest black mypy

This creates a new dependency-groups section:

pyproject.toml

[dependency-groups]
dev = [
    "black>=25.1.0",
    "mypy>=1.15.0",
    "pytest>=8.3.4",
]

Now, these tools are part of the dev group and won’t be installed unless explicitly requested.

When working in development, you can run tools installed with --dev using:

 
uv run --dev pytest

This ensures that pytest runs only when needed, preventing unnecessary tools from cluttering production.

You can create different groups for various tools. For example, a linting group for formatters and linters:

 
uv add --group lint ruff flake8

The command adds ruff and flake8 to a new lint dependency group, keeping them separate from production dependencies. This ensures they are only installed when explicitly requested, preventing unnecessary bloat.

Resulting in:

pyproject.toml

[dependency-groups]
dev = [
    "black>=25.1.0",
    "mypy>=1.15.0",
    "pytest>=8.3.4",
]
lint = [
    "flake8>=7.1.1",
    "ruff>=0.9.6",
]

You can now run linting tools using the --group lint flag:

 
uv run --group lint flake8 hello.py 

This keeps your environment organized and efficient. When you leveraging dependency groups in uv, you can manage dependencies in a more structured, scalable, and efficient way.

Managing multiple Python versions

Managing multiple Python versions can be challenging, especially when different projects require different versions. uv simplifies Python version management by automatically detecting, installing, and switching between Python versions based on project requirements.

Unlike tools like pyenv, uv integrates Python version control with dependency management, ensuring a consistent and optimized development experience.

If Python is installed on your system, uv will automatically detect and use it without requiring additional configuration. To check the current Python version being used by uv, run the following:

 
uv run python --version

While uv automatically detects system-installed Python versions, you can also install specific versions. To install the latest Python version:

 
uv python install

Or install a specific version:

 
uv python install 3.12
Output
Installed Python 3.12.9 in 45.18s
 + cpython-3.12.9-macos-aarch64-none

However, installing a version doesn't automatically make your project use it. If you run:

 
uv run python --version
Output
Python 3.13.2

At this point, Python 3.12 is installed, but it is not automatically set as the default version for the project.

This means the project is still using the latest available version. To ensure the project uses Python 3.12, define it in pyproject.toml:

 
[project]
requires-python = ">=3.12"

You can then pin the specific version using:

 
uv python pin 3.12

After pinning, check the version again:

 
uv run python --version

Now, uv will update the project environment and dependencies to match the selected Python version:

Output
Using CPython 3.12.9
Removed virtual environment at: .venv
Creating virtual environment at: .venv
Installed 22 packages in 126ms
Python 3.12.9

Automatic Python installation with uv

Its automatic Python installation is one of uv’s most powerful features. If a required Python version is missing, uv will download and install it on demand, ensuring your project always runs in the correct environment.

For example, if Python 3.11 is not installed, running the following command will automatically install Python 3.11 before executing the command:

 
uvx python@3.11 -c "print('Hello, world!')"

Similarly, if no Python version is available on your system, simply creating a virtual environment will install the latest Python version and set up the environment without requiring manual intervention.

 
uv venv

If you prefer manual control over Python installations, you can disable automatic downloads by running:

 
uv --no-python-downloads

This ensures uv only uses pre-installed Python versions, giving you full control over your development environment.

By using uv to manage Python versions, you no longer have to deal with the hassle of manually installing and switching between versions.

uv handles everything for you, ensuring a stable, predictable, and fully optimized development environment.

Final thoughts

This article explored key features of uv to help you modernize your Python workflow. With uv, you can manage packages, virtual environments, and Python versions with a single, efficient tool.

To explore the tool in detail, check out the official documentation.

Thanks for reading!

Author's avatar
Article by
Stanley Ulili
Stanley Ulili is a technical educator at Better Stack based in Malawi. He specializes in backend development and has freelanced for platforms like DigitalOcean, LogRocket, and AppSignal. Stanley is passionate about making complex topics accessible to developers.
Got an article suggestion? Let us know
Next article
Load Testing with Locust: A High-Performance, Scalable Tool for Python
Learn how to perform high-concurrency load testing with Locust, an open-source Python framework. This step-by-step guide covers setting up Locust, creating test scenarios, analyzing performance metrics, and automating load tests to optimize API performance
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github