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
Hide 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.