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.tomlupdated automatically when installing packages, and how do I migrate from an existingrequirements.txt?
This guide explains the clean workflow.
uv is becoming popular because it:
pip, virtualenv, pip-tools)pyproject.toml + lockfile for reproducible environmentsA 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:
| File | Purpose |
|---|---|
pyproject.toml | Direct dependencies you choose |
uv.lock | Fully resolved dependency tree |
You only maintain direct dependencies.
These are the common steps you'll use when creating and managing a Python project with uv.
uv init
This creates:
[project]
name = "my-project"
version = "0.1.0"
dependencies = []
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:
pyproject.tomluv.lockExample:
[project]
dependencies = [
"fastapi>=0.110.0",
]
uv pip install for temporary tools or debugging.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:
[dependency-groups]
dev = [
"pytest>=8.0.0",
]
When deploying to production, only regular dependencies are installed (unless you explicitly request dev deps).
If you added a package by mistake:
uv remove fastapi
This updates both pyproject.toml and uv.lock.
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
Specify which Python version your project needs:
[project]
requires-python = ">=3.11,<3.13"
uv will automatically download and use the correct Python version if needed.
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.
Most existing Python projects already have a requirements.txt. The mistake is copying everything into pyproject.toml.
Instead, migrate like this.
Create your project and import everything. uv will put everything into your pyproject.toml initially.
uv init
uv add -r requirements.txt
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
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
Generate the deterministic uv.lock file and confirm the hierarchy is clean.
uv lock
uv tree
[project]
name = "my-project"
version = "0.1.0"
dependencies = [
"fastapi>=0.110.0",
]
[dependency-groups]
dev = [
"ruff>=0.5.0",
]
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.