TechGuild
1Level 1, Curious Newcomer

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.

11 min readfoundationFacts checked August 2026
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 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 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 , pinned to the minor series of the uv that wrote the file. What a does, and how a 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 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 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 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. 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 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 and rewrites no manifest, so the diff stays empty.

What you knowWhat Python does
package.json plus a lockfilepyproject.toml plus uv.lock
npm install, npm runuv add, uv run
npx eslintuvx ruff
node_modules per project.venv, a directory of installed packages rather than vendored source
Maven coordinates and the classpathNo 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 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.