Getting Started with uv

A beginner-friendly guide to using uv to manage Python dependencies with pyproject.toml, install packages the right way, and migrate cleanly from the traditional pip + requirements.txt workflow.

If you're already experienced with uv or modern Python dependency management, this post might be too basic for you. I'm writing this mainly for beginners (like I was) and people migrating from the traditional pip + requirements.txt workflow.

When I started using uv, one question immediately came up:

How do I keep pyproject.toml updated automatically when installing packages, and how do I migrate from an existing requirements.txt?

This guide explains the clean workflow.

Why uv?

uv is becoming popular because it:

  • is extremely fast (written in Rust)
  • replaces multiple tools (pip, virtualenv, pip-tools)
  • uses pyproject.toml + lockfile for reproducible environments

Key: Direct vs Transitive Dependencies

A typical requirements.txt might contain many packages:

fastapi
pydantic
starlette
anyio
typing_extensions

But usually you only installed something like:

fastapi

The rest are transitive dependencies.

With modern tools like uv:

FilePurpose
pyproject.tomlDirect dependencies you choose
uv.lockFully resolved dependency tree

You only maintain direct dependencies.

Basic uv Workflow

These are the common steps you'll use when creating and managing a Python project with uv.

Initialize a Project

uv init

This creates:

pyproject.toml
[project]
name = "my-project"
version = "0.1.0"
dependencies = []

Install Packages (Correct Way)

The difference between uv add and uv pip install determines whether your project stays maintainable.

Do not use:

uv pip install fastapi

That behaves like pip and does not update pyproject.toml. Later, when someone else clones your project, they won't know which packages are needed.

Instead use:

uv add fastapi

This will:

  • Add the dependency to pyproject.toml
  • Update uv.lock
  • Install the package into your environment

Example:

pyproject.toml
[project]
dependencies = [
  "fastapi>=0.110.0",
]
Only use uv pip install for temporary tools or debugging.

Add Dev Dependencies

Dev dependencies are packages needed for development and testing, but not required when your code runs in production. Examples: testing frameworks, linters, formatters.

uv add --dev pytest

This adds the package to a separate dependency group:

pyproject.toml
[dependency-groups]
dev = [
  "pytest>=8.0.0",
]

When deploying to production, only regular dependencies are installed (unless you explicitly request dev deps).

Remove Dependencies

If you added a package by mistake:

uv remove fastapi

This updates both pyproject.toml and uv.lock.

Update Dependencies

Update all packages to their latest compatible versions:

uv lock --upgrade

Then sync your environment:

uv sync

Update a single package:

uv lock --upgrade-package fastapi
uv sync

Pin Python Version

Specify which Python version your project needs:

pyproject.toml
[project]
requires-python = ">=3.11,<3.13"

uv will automatically download and use the correct Python version if needed.

Sync the Environment

Sync your local environment to match pyproject.toml and uv.lock:

uv sync

For CI (using exact locked versions):

uv sync --frozen

This ensures the environment matches the exact versions in uv.lock. Use this in production and CI pipelines for reproducibility.

Clean Migration from requirements.txt

Most existing Python projects already have a requirements.txt. The mistake is copying everything into pyproject.toml.

Instead, migrate like this.

Initialize the uv Project & Add Dependencies

Create your project and import everything. uv will put everything into your pyproject.toml initially.

uv init
uv add -r requirements.txt

Identify the Bloat

Run the tree command.

uv tree

Look for packages at the root (left margin) that also appear nested under your main tools. In your example, annotated-doc and typing-inspection are root-level duplicates.

my-project v0.1.0
├── annotated-doc v0.0.4
├── annotated-types v0.7.0
├── anyio v4.12.1
│   └── idna v3.11
├── fastapi v0.135.1
│   ├── annotated-doc v0.0.4
│   ├── pydantic v2.12.5
│   │   ├── ...
│   ├── starlette v0.52.1
│   │   └── ...
│   ├── typing-extensions v4.15.0
│   └── typing-inspection v0.4.2 (*)
├── idna v3.11
├── pydantic v2.12.5 (*)
├── pydantic-core v2.41.5 (*)
├── starlette v0.52.1 (*)
├── typing-extensions v4.15.0
└── typing-inspection v0.4.2 (*)
(*) Package tree already displayed

Prune Transitive Dependencies

Remove the packages that should be nested. uv will remove them from pyproject.toml but keep them in the environment because your main packages still require them.

# Example: removing sub-deps of FastAPI
uv remove annotated-doc typing-inspection pydantic starlette anyio idna

After pruning, your tree collapses. Your pyproject.toml is now human-readable, containing only the packages you actually care about.

my-project v0.1.0
├── annotated-doc v0.0.4
├── fastapi v0.135.1
│   ├── annotated-doc v0.0.4
│   ├── pydantic v2.12.5
│   │   ├── ...
│   ├── starlette v0.52.1
│   │   └── ...
│   ├── typing-extensions v4.15.0
│   └── typing-inspection v0.4.2 (*)
└── typing-inspection v0.4.2 (*)
(*) Package tree already displayed

Lock and Verify

Generate the deterministic uv.lock file and confirm the hierarchy is clean.

uv lock
uv tree

Final Project Structure

my-project/pyproject.toml
[project]
name = "my-project"
version = "0.1.0"
dependencies = [
  "fastapi>=0.110.0",
]

[dependency-groups]
dev = [
  "ruff>=0.5.0",
]

Format Your Code with Ruff

You can add Ruff as a dev dependency:

uv add --dev ruff

Then format your project with:

ruff format

Ruff is developed by the same team behind uv, and is designed to be extremely fast while providing formatting and linting in one tool.

#python #uv #ruff #astral