Skip to content
← Back to explainers

Python explainer

Manage a Project with uv

About 5 min to read. Allow extra time to work through examples.

Use uv to pick a Python, create a project, add dependencies and run commands. Then make CI fail when uv.lock is out of date, and give a single script its own dependencies.

This article uses Python. You can make it your practice mode to browse related content across the site.

Setting up a Python project by hand takes several steps. You install the right Python, make a virtual environment, install your dependencies with pip, and try to remember which versions worked. uv does all of this from one command-line tool. Start a new application:

uv init --app --no-package --python 3.14 demo-app
cd demo-app
uv run main.py

uv run finds Python 3.14, or downloads it if it has to. It creates a virtual environment in .venv, installs the project's dependencies there, and runs main.py with that environment. You never activate it yourself.

uv init also writes a .python-version file, which records the Python the project prefers. requires-python in pyproject.toml lists every version the project supports. A 3.14 pin accepts any 3.14 release, not one exact patch release. If a CI machine has no internet access, install its Python in advance with uv python install 3.14.

Choose a project layout

In uv 0.13, each of these commands creates a different starting project. Run each one in its own new folder:

Command What it creates
uv init --app --no-package --python 3.14 An application with a top-level main.py, without a build system
uv init --app --python 3.14 A packaged application with a src/ layout and a command in [project.scripts]
uv init --lib --python 3.14 A library with a src/ layout and a build system
uv init --bare --python 3.14 Only a minimal pyproject.toml

The packaged layouts use src/, which stops your tests from importing the source folder by accident. See creating projects.

Add dependencies with uv add

From the project folder:

uv add httpx
uv add --dev pytest
uv add --group typing mypy
uv run --group typing mypy main.py
uv run pytest

uv add httpx adds httpx to dependencies in pyproject.toml, installs it, and updates uv.lock. That file is uv's lockfile. It records the exact version of every package the project installs, including the dependencies of your dependencies. A later article covers lockfiles. Commit pyproject.toml, uv.lock and .python-version.

--dev and --group put a package in a dependency group instead. A dependency group is a named list of packages for work on the project, such as tests, and it is not published with the package. uv installs the dev group by default. Name any other group when you run its tool, as in uv run --group typing. uv init creates no tests, so write one in tests/ before you run pytest.

uv pip install also exists. It installs a package into the environment without recording it in pyproject.toml or uv.lock, so the next checkout does not get it. Use uv add for anything the project needs.

Make CI fail when the lockfile is out of date

uv sync installs the locked packages into .venv. Three forms treat uv.lock differently:

Command What it does with uv.lock
uv sync Creates or updates the lockfile if needed, then installs
uv sync --locked Fails if the lockfile is missing or out of date
uv sync --frozen Installs from the lockfile as it is, without checking it against pyproject.toml

In CI, use --locked on every command:

uv sync --locked
uv run --locked pytest

Plain uv sync in CI can rewrite uv.lock inside the CI checkout, and then the job tests versions nobody committed. With --locked, the job fails, and the developer runs uv lock and commits the result. uv sync also removes packages that the lockfile does not list. uv run installs what is missing but leaves extra packages in place. See locking and syncing.

Run a tool without adding it to the project

uvx ruff check .

uvx installs a tool into its own cached environment and runs it. The project's lockfile does not pin that tool's version. To give the whole team the same Ruff, add it to a dependency group and run it with uv run. For a tool you use in every project, uv tool install ruff installs it once, and the folder uv puts it in must be on your PATH. See tool environments.

Give a script its own dependencies

A one-file script can carry its own dependency list. Outside any project, run:

uv init --script --python 3.14 analyse.py
uv add --script analyse.py httpx
uv run analyse.py

uv init --script writes a comment block at the top of the script with the Python version, and uv add --script adds the dependencies to it. Packaging tools call this block inline script metadata. uv reads it and runs the script in its own environment, apart from any project. To record exact versions too, run uv lock --script analyse.py, keep the analyse.py.lock file next to the script, and run it with uv run --locked analyse.py. See running scripts.

Check your understanding Why might uv run analyse.py work while python analyse.py fails? Show answer

The two commands can use different Pythons and different environments. uv run reads the script's inline metadata and installs the dependencies it lists. python analyse.py ignores that comment block, so an import fails if your current environment lacks the package. Check sys.executable and the installed packages before you change the script.

step 26 of 41

Modules, Packaging and Tooling

Up next Choose Version Ranges for Your Dependencies