A project in five minutes
uv creates the project, the virtual environment, the lockfile and the entry point, and runs your code without you ever activating anything. This lesson gets a real, packaged, runnable project on disk and names what each generated file is for.
On this page
What you will be able to do
- Create a packaged project with uv and explain every file it generates
- Add runtime and development dependencies to the right table
- Run a project entry point and a single-file script without activating an environment
- State honestly where uv sits in the 2026 tool landscape
There is no npm init blessed by the language and no archetype to choose from. What there is, in
2026, is uv: one binary for the project, the environment, the lockfile and running your code. Install
it with the instructions at docs.astral.sh/uv, then:
uv init demo
cd demo
That is the setup. Everything below is what those two lines actually produced, on uv 0.12.3 against CPython 3.14.7, checked on 13 August 2026.
What uv init writes
demo/
.git/
.gitignore
.python-version
README.md
pyproject.toml
src/demo/__init__.py
Three of those repay a closer look. .python-version holds one line naming the interpreter for this
directory, 3.14 here, and it is what uv reads on every later command. uv picks from what it finds,
so pin it with uv init --python 3.14 demo when the choice matters; uv python install downloads a
version the machine lacks. src/demo/__init__.py is the package, and it already contains a main
function that prints a greeting. Note what is missing: no loose main.py. A packaged application is
the default now, which is the biggest difference between this tree and the one an older tutorial
shows you.
pyproject.toml is the whole manifest, and it arrives already able to build and install itself:
[project]
name = "demo"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = [
{ name = "Your Name", email = "[email protected]" }
]
requires-python = ">=3.14"
dependencies = []
[project.scripts]
demo = "demo:main"
[build-system]
requires = ["uv_build>=0.12.3,<0.13.0"]
build-backend = "uv_build"
The authors entry is filled in from your git configuration. requires-python follows
.python-version. [project.scripts] declares a console command called demo that calls main in
the demo package, so an entry point exists before you have written a line of code. The
[build-system] table names uv's own build backend, pinned to the minor series of the uv that wrote
the file. What a build backend does, and how a wheel gets to anyone else, belongs to
packaging and distribution.
Dependencies go in one of two tables
uv add httpx
uv add --dev pytest ruff ty
The first writes "httpx>=0.28.1" into [project.dependencies]. The second does not touch
[project.optional-dependencies], which is where a reader coming from an older Python project will
expect it: it creates a dev group under [dependency-groups], the table Python Enhancement Proposal (PEP) 735 standardised.
- dependency group
A named set of dependencies declared in
[dependency-groups]for people working on the project rather than for people installing it. Groups are not published as part of the distribution, so nothing in one can reach a user of your wheel.
The distinction is who the dependency is for. An optional dependency is an offer to your users, asked for by name at install time. A dependency group is for the repository: test runners, linters, type checkers, the things CI needs and nobody downstream does.
The version bounds uv writes are whatever resolved at the time. On 13 August 2026 that was httpx 0.28.1, pytest 9.1.1, ruff 0.16.2 and ty 0.0.71. Treat that last one accordingly: ty is Astral's type checker, it is beta, and it is still on a 0.0.x version. Choosing between it and the alternatives is the job of ruff and type checking in the loop.
Running it
$ uv run demo
Hello from demo!
uv run syncs before it runs. If .venv is missing it is created, and if pyproject.toml has moved
ahead of uv.lock the project is relocked and reinstalled first. There is no activate step, so there
is no activate step to forget. Anything else you want to run inside that environment goes after
uv run: uv run python, uv run pytest, uv run ruff check.
uv init gives you a console command but not a src/demo/__main__.py, so uv run python -m demo
fails until you add one:
from demo import main
main()
The import trap this layout creates
A file started by path puts that file's own directory at the front of sys.path. The same file
started with -m gets your current directory there instead, and the package is imported by name from
the installed project. Same code, two import contexts, and the second one is the only one that
matches how your package will be imported in production.
The src layout is what makes this strict. With the code one level down, the working directory is
not importable, so an import can only resolve against the installed project. That is the point: the
thing you test is the thing you ship.
The mechanism underneath, along with circular imports and the __main__ guard, is
modules, packages and imports.
The lockfile is the record
uv lock writes uv.lock: readable TOML holding the resolved version, the hash and the wheel URL of
every dependency, for every platform the project supports rather than only yours. Commit it, and
never hand-edit it. uv sync makes the environment match it exactly, including removing packages
that are no longer named, which is what a fresh machine and a CI job both want. uv lock --check
asserts that the lockfile is current without changing anything.
Tools that are not dependencies
A linter is not part of your program. uv tool install ruff puts ruff on your PATH in an environment
of its own, with nothing entering the project. uvx ruff check is the short form of
uv tool run ruff check, which fetches the tool if it is not cached, runs it, and installs nothing
permanently. Pin inline where the version matters: uvx [email protected] check.
One file, no project
Not everything deserves a project. PEP 723 lets a single file declare its own requirements in a TOML
block inside a comment, opened by # /// script and closed by # ///:
# /// script
# requires-python = ">=3.14"
# dependencies = [
# "rich>=15.0.0",
# ]
# ///
uv init --script report.py --python 3.14 writes that block empty, uv add --script report.py rich
edits it in place, and uv run report.py builds a throwaway environment from it and executes the
file. The recipient needs uv and nothing else.
Where uv sits
uv is 0.x, deliberately so, with no announced 1.0 date. The clearest evidence of what that means is
in the file it just wrote for you: the [build-system] bound stops at the next minor series, which
is uv budgeting for its own breaking changes. It is still the fastest honest answer to how you start
a Python project in 2026, and you should expect to meet pip with a venv, Poetry and pipenv in
repositories that already exist. None of those are broken, and none of them need replacing on the day
you join. You will clone one on day one, so here is the install step. A repository with a
requirements.txt and no lockfile expects the interpreter's own tools:
python -m venv .venv
source .venv/bin/activate # .venv\Scripts\activate on Windows
pip install -r requirements.txt
Activation is a shell trick rather than a property of the project. The script prepends .venv/bin to
PATH and sets VIRTUAL_ENV, so python and pip resolve inside the environment for as long as
that shell lives. A second terminal knows nothing about it, and .venv/bin/python works without it,
which is why a cron entry names that path instead of trying to activate anything.
If the repository is a package rather than a pile of scripts, that install is not enough. A src
layout keeps the code one level down, so nothing at the root is importable and the tests fail on
their first import. pip install -e . installs the project in editable mode: importable by name,
with your edits live and no reinstall. Poetry and pipenv fold both steps into one command,
poetry install and pipenv install, and create the environment themselves.
uv does all three, so you can keep the tool you just learnt:
uv venv
uv pip install -r requirements.txt
uv pip install -e .
uv pip is a drop-in for pip's command line, against whatever environment uv finds: VIRTUAL_ENV
first, then .venv here or in a parent. It writes no lockfile and rewrites no manifest, so the diff
stays empty.
| What you know | What Python does |
|---|---|
package.json plus a lockfile | pyproject.toml plus uv.lock |
npm install, npm run | uv add, uv run |
npx eslint | uvx ruff |
node_modules per project | .venv, a directory of installed packages rather than vendored source |
| Maven coordinates and the classpath | No equivalent at all, which is the point of the environment |
The last row is the one worth sitting with. Java resolves imports against a classpath you assemble; Python resolves them against whatever is installed in the interpreter that is running, and the virtual environment exists so that "whatever is installed" means this project and nothing else. Which interpreter that is, and why it matters, is the runtime you are targeting.
Check your understanding
Sign in to take this check
4 questions on this lesson, one at a time, with the reasoning for every option as soon as you answer. Each answer is marked on the server and stored against your account.
An account is free. There is no paid plan, no tier and nothing to buy.