Type something to search...
Managing Python Projects with uv: The Fast All-in-One Tool

Managing Python Projects with uv: The Fast All-in-One Tool

Setting up a Python project used to involve a small stack of tools: pyenv to get the right Python version, venv to create an environment, pip to install packages, pip-tools or Poetry to lock versions, pipx to install command-line tools, and twine to publish. Each one works, but together they're a lot to learn and keep in sync.

uv, from Astral (the team behind the Ruff linter), replaces that whole stack with a single binary written in Rust. It manages Python installations, virtual environments, dependencies, lock files, scripts, and tools, and it's fast enough that installs which took a minute with pip often finish in a second or two.

This guide walks through uv the way you'd actually use it: creating a project, adding dependencies, understanding the lock file, running code, working with standalone scripts, managing Python versions, and running tools. The commands and output here come from uv 0.12.

Installing uv

uv is a standalone binary, so it doesn't need Python to be installed first. The official installers:

# macOS and Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

It's also available through Homebrew (brew install uv), WinGet, and pipx, or with pip install uv. Check that it works:

uv --version
uv 0.12.22 (70fe1196a 2026-10-01 aarch64-apple-darwin)

Run uv self update later to upgrade a standalone install.

Creating a Project

uv init scaffolds a new project:

uv init weather-cli
cd weather-cli

You get a small, standard layout:

weather-cli/
├── .git/
├── .gitignore
├── .python-version
├── README.md
├── pyproject.toml
└── src/
    └── weather_cli/
        └── __init__.py

The generated pyproject.toml is plain, standards-based configuration, not a uv-specific format:

[project]
name = "weather-cli"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = [
    { name = "Your Name", email = "you@example.com" }
]
requires-python = ">=3.14"
dependencies = []

[project.scripts]
weather-cli = "weather_cli:main"

[build-system]
requires = ["uv_build>=0.12.22,<0.13.0"]
build-backend = "uv_build"

A few things to notice:

  • requires-python is set from the Python version uv picked. .python-version pins the exact version the project uses locally.
  • Applications are created as installable packages with a src/ layout and a [project.scripts] entry point, so weather-cli becomes a real command.
  • uv init also initializes a Git repository unless you pass --vcs none.

The variations you're likely to want:

CommandCreates
uv initA packaged application (the default)
uv init --no-packageA simple app with a top-level main.py and no build system
uv init --libA library, with a py.typed marker, meant to be published
uv init --bareOnly a pyproject.toml
uv init --script tool.pyA single script with inline metadata (covered below)

Every field in pyproject.toml is explained in Understanding pyproject.toml.

Running Code with uv run

You don't need to create or activate a virtual environment yourself. uv run does it for you:

uv run weather-cli
Using CPython 3.14.8
Creating virtual environment at: .venv
Hello from weather-cli!

On the first run, uv creates .venv, installs the project and its dependencies into it, and then runs the command. On every later run, it checks that the environment matches the lock file (which takes milliseconds when nothing changed) and syncs it if needed.

uv run works with any command:

uv run python -c "import sys; print(sys.version)"
uv run python app.py
uv run pytest
uv run ruff check .

The .venv directory is a completely standard virtual environment. If you prefer, you can still activate it with source .venv/bin/activate and use python and pytest directly. How environments work is covered in Virtual Environments in Python: venv Explained.

Managing Dependencies

Adding and Removing Packages

uv add httpx
Resolved 8 packages in 645ms
Prepared 8 packages in 3.29s
Installed 8 packages in 4ms
 + anyio==4.15.1
 + certifi==2026.7.22
 + h11==0.16.0
 + httpcore==1.0.9
 + httpx==0.28.1
 + idna==3.20
 + typing-extensions==4.16.0

That single command does three things: adds "httpx>=0.28.1" to dependencies in pyproject.toml, resolves the full dependency tree and writes uv.lock, and installs everything into .venv.

You can pass version constraints, extras, and other sources:

uv add "httpx>=0.28"
uv add "fastapi[standard]"
uv add "git+https://github.com/encode/httpx"
uv add --editable ../shared-utils

Removing a dependency cleans up the manifest, the lock file, and the environment:

uv remove httpx

Development Dependencies

Tools you need for development but not in production, like pytest and Ruff, go in a dependency group:

uv add --dev pytest ruff

uv writes them to the standard [dependency-groups] table (PEP 735):

[dependency-groups]
dev = [
    "pytest>=9.1.1",
    "ruff>=0.16.10",
]

The dev group is installed by default with uv sync and uv run. You can create other groups with --group, for example uv add --group docs mkdocs, and include them with uv sync --group docs. Optional features for users of your package go in [project.optional-dependencies] with uv add --optional.

Seeing the Dependency Tree

uv tree
weather-cli v0.1.0
├── httpx v0.28.1
│   ├── anyio v4.15.1
│   │   ├── idna v3.20
│   │   └── typing-extensions v4.16.0
│   ├── certifi v2026.7.22
│   ├── httpcore v1.0.9
│   │   ├── certifi v2026.7.22
│   │   └── h11 v0.16.0
│   └── idna v3.20
├── pytest v9.1.1 (group: dev)
│   ├── iniconfig v2.3.0
│   ├── packaging v26.3
│   ├── pluggy v1.6.0
│   └── pygments v2.21.0
└── ruff v0.16.10 (group: dev)

uv tree --outdated shows which packages have newer versions available, and uv tree --invert --package idna shows which packages pull a given dependency in.

The Lock File: uv.lock

pyproject.toml declares what you want (httpx>=0.28). uv.lock records exactly what you got: every package, direct and indirect, with exact versions, source URLs, and hashes.

version = 1
revision = 5
requires-python = ">=3.14"

[[package]]
name = "anyio"
version = "4.15.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "idna" },
    { name = "typing-extensions", marker = "python_full_version < '3.15'" },
]

Some important properties:

  • It's universal. A single lock file covers every platform and Python version allowed by requires-python. Platform-specific dependencies are recorded with markers rather than requiring a separate lock per OS.
  • Commit it. For applications, the lock file is what makes builds reproducible across machines and CI.
  • Don't edit it by hand. uv manages it.

Syncing, Locking, and Upgrading

The commands that work with the lock file:

CommandWhat it does
uv lockResolve dependencies and update uv.lock without installing
uv syncMake .venv match the lock file exactly (installs and removes packages)
uv sync --lockedSync, but fail if uv.lock is out of date with pyproject.toml
uv sync --frozenSync from uv.lock as-is without checking it against pyproject.toml
uv sync --no-devSkip the dev group (for production)
uv lock --upgradeUpgrade all packages to the newest versions allowed
uv lock --upgrade-package httpxUpgrade one package only
uv lock --checkExit with an error if the lock file needs updating

When a teammate clones your repository, they run one command:

uv sync

uv installs the Python version from .python-version if it isn't present, creates .venv, and installs exactly what's in the lock file. In CI, use uv sync --locked so the build fails loudly if someone changed pyproject.toml without updating the lock.

If another tool needs a requirements.txt, export one:

uv export --no-hashes --no-dev > requirements.txt
# This file was autogenerated by uv via the following command:
#    uv export --no-hashes --no-dev
-e .
anyio==4.15.1
    # via httpx
certifi==2026.7.22
    # via
    #   httpcore
    #   httpx
...

Managing Python Versions

uv can download and manage Python interpreters itself, replacing tools like pyenv for most purposes:

uv python install 3.13        # install a version
uv python install 3.14t       # the free-threaded build
uv python list                # show installed and available versions
uv python pin 3.13            # write .python-version for this project

You rarely need to install versions explicitly. If a project requires a Python you don't have, uv run and uv sync download it automatically. uv also finds interpreters already on your system, such as Homebrew or python.org installs.

uv python pin refuses versions that conflict with requires-python, which catches a common mistake early:

uv python pin 3.13
error: The requested Python version `3.13` is incompatible with the project `requires-python` value of `>=3.14`.

To support 3.13, lower requires-python first, then pin.

You can also run a one-off command on a different Python without changing the project: uv run --python 3.13 pytest. That's handy for checking compatibility before widening requires-python.

Scripts with Inline Dependencies

Not everything needs a project. For single-file scripts, uv supports inline script metadata (PEP 723), where dependencies are declared in a comment block at the top of the file.

Start with a plain script:

# fetch_peps.py
import httpx

response = httpx.get("https://peps.python.org/api/peps.json", timeout=10)
peps = response.json()
print(f"{response.status_code}: {len(peps)} PEPs")

Add a dependency to it:

uv add --script fetch_peps.py httpx

uv writes the metadata block into the file:

# /// script
# requires-python = ">=3.13"
# dependencies = [
#     "httpx>=0.28.1",
# ]
# ///
import httpx

response = httpx.get("https://peps.python.org/api/peps.json", timeout=10)
peps = response.json()
print(f"{response.status_code}: {len(peps)} PEPs")

Now anyone with uv can run it, and uv builds a cached, isolated environment with the right dependencies on the fly:

uv run fetch_peps.py
200: 745 PEPs

The script is fully self-describing. There's no separate requirements.txt to keep next to it and no environment to set up. It's a great fit for automation scripts and utilities you share with teammates.

For quick experiments, --with adds packages for a single run without touching any file:

uv run --with rich python -c "import rich; print('rich ok')"

Running and Installing Tools

Command-line tools written in Python, like Ruff, Black, httpie, or pre-commit, shouldn't be dependencies of your project. uv handles them separately, much like pipx.

uvx (an alias for uv tool run) runs a tool in a temporary, cached environment:

uvx ruff --version
uvx ruff check .
uvx --from httpie http GET https://example.com
ruff 0.16.10

You can pin a version with uvx ruff@0.16.10 check .. For tools you use all the time, install them permanently so they're on your PATH:

uv tool install ruff
uv tool list
uv tool upgrade --all

Each tool gets its own isolated environment, so their dependencies never conflict with each other or with your projects.

Building and Publishing

uv can build and publish packages too:

uv version --bump minor   # 0.1.0 => 0.2.0, updates pyproject.toml
uv build                  # writes an sdist and a wheel to dist/
uv publish                # uploads dist/* to PyPI
Successfully built dist/weather_cli-0.1.0.tar.gz
Successfully built dist/weather_cli-0.1.0-py3-none-any.whl

uv build works with any standards-compliant build backend, not just uv_build, so it also builds Hatchling, setuptools, or Poetry-based projects. uv publish supports API tokens and trusted publishing from GitHub Actions.

The pip-Compatible Interface

If you have existing workflows built around pip and requirements.txt, uv offers drop-in replacements under uv pip and uv venv:

uv venv                               # create .venv
uv pip install -r requirements.txt    # install into it
uv pip compile requirements.in -o requirements.txt
uv pip sync requirements.txt
uv pip list

These behave like pip, venv, and pip-tools, but much faster. They're a low-risk way to speed up an existing project without changing its structure. The project commands (uv add, uv sync, uv run) are the better choice for new work, because they keep pyproject.toml, the lock file, and the environment in sync automatically. For how uv compares to Poetry and pip-tools in more depth, see Poetry vs uv vs pip-tools.

Using uv in CI and Docker

In GitHub Actions, the official astral-sh/setup-uv action installs uv and can cache its downloads:

# .github/workflows/test.yml
name: test
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: astral-sh/setup-uv@v10
      - run: uv sync --locked
      - run: uv run pytest

In a Dockerfile, copy the uv binary from its official image and sync from the lock file:

FROM python:3.13-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --locked --no-dev --no-install-project

COPY . .
RUN uv sync --locked --no-dev

CMD ["uv", "run", "--no-dev", "weather-cli"]

Installing dependencies before copying the source lets Docker cache that layer, so code changes don't trigger a full reinstall.

A Day-to-Day Cheat Sheet

TaskCommand
New projectuv init myapp
Add a dependencyuv add requests
Add a dev dependencyuv add --dev pytest
Remove a dependencyuv remove requests
Install everything from the lockuv sync
Run a command in the projectuv run pytest
Upgrade one packageuv lock --upgrade-package requests
Show the dependency treeuv tree
Pin the Python versionuv python pin 3.13
Run a script with inline depsuv run script.py
Run a tool onceuvx ruff check .
Build a packageuv build

Conclusion

uv collapses the Python packaging toolchain into one fast tool. uv init creates a standards-based project, uv add and uv remove keep pyproject.toml and uv.lock in sync, uv run makes sure the environment is correct before every command, and uv sync reproduces the exact environment on any machine. On top of that, it installs Python versions, runs self-contained scripts with inline metadata, and manages command-line tools.

Because everything it produces is standard, pyproject.toml, .venv, wheels, there's little lock-in: the same project still works with pip and other tools. If you're starting a new Python project today, uv is a very reasonable default.

Tags :
Share :

Related Posts

Abstract Base Classes in Python with the abc Module

Abstract Base Classes in Python with the abc Module

Python leans on duck typing: if an object has the method you need, you call it and move on. That works well until you have a family of classes that a

Continue Reading
*args and **kwargs in Python: Flexible Function Signatures

*args and **kwargs in Python: Flexible Function Signatures

You've seen def wrapper(*args, **kwargs): in decorators, and probably super().__init__(**kwargs) in class hierarchies. These two parameters let a

Continue Reading
Asyncio in Python: A Beginner's Guide to Asynchronous Programming

Asyncio in Python: A Beginner's Guide to Asynchronous Programming

A lot of programs spend most of their time waiting. A web scraper waits for pages to download, an API server waits for the database, a chat bot waits

Continue Reading