New in 2026: Master Python for AI, Data Science

Programming

Starting a Python Project with uv from Scratch

Starting a Python Project with uv from Scratch

You run uv init, and in under a second you have a complete project scaffolded — .gitignore, Python version pinning, a project config file, and your first script ready to run. No manual folder creation. No hunting for the right config format. No forgotten pip install.

In this hands-on guide, you will build a Python project from zero using uv — the Rust-powered tool that replaces pip, virtualenv, pyenv, and poetry in a single 2MB binary. Every command is real. Every file is explained.

In this tutorial, you will learn to:

  • Scaffold a complete Python project with uv init
  • Understand every file it creates and why each one matters
  • Run scripts with uv run — no manual environment activation
  • Manage and switch Python versions with uv python
  • Migrate an existing requirements.txt project to uv
  • Bootstrap a working FastAPI app end-to-end

What Does uv init Actually Create?

Fire up your terminal and run:

uv init my-project
cd my-project
ls -la

uv creates exactly four files. Here is what each one does.

The Scaffolded Files

my-project/
├── .gitignore          # Keeps .venv/, __pycache__, .env out of git
├── .python-version     # Pins the exact Python runtime version
├── pyproject.toml      # Project metadata + dependency declarations
└── README.md           # Auto-generated placeholder

Note: Run uv init --help to see all options: --no-readme, --no-git, --package (creates an installable library project instead of a script), and --name to override the project name.

uv does not create a uv.lock file at this stage — the lockfile is generated when you first run uv sync or uv lock.

Project Structure Deep Dive

Let us walk through each file in detail — what it contains, why it exists, and how uv uses it. Every file has a purpose, and understanding them individually makes you far more effective when things go wrong or when you need to customise the scaffold.

pyproject.toml — Your Project in One File

Open pyproject.toml. It looks like this out of the box:

[project]
name = "my-project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []

This replaces the old stack of setup.py, requirements.txt, and MANIFEST.in. Everything lives in one file following PEP 621.

The key fields:

  • name — used when installing the package (uv pip install my-project)
  • version — follows Semantic Versioning
  • requires-python — declares the minimum Python version, used by pip and index servers
  • dependencies — your runtime packages. Add them with uv add <package>

You can also add a [dependency-groups] section for dev-only tools (pytest, ruff):

[dependency-groups]
dev = ["pytest", "ruff"]

.python-version — Pin Your Runtime

This single line file pins the Python interpreter version:

3.12

When you run uv sync or uv run, uv reads this file and automatically provisions the correct Python version. No pyenv, no python3.12 path hunting.

.venv/ — Your Isolated Environment

uv creates the .venv/ directory automatically on first run. It is your project-specific Python environment — completely isolated from system Python and other projects.

.venv/
├── bin/           # Unix: python, pip, pytest executables
│                  # Windows: Scripts/python.exe, Scripts/pip.exe
├── lib/
│   └── python3.12/
│       └── site-packages/  # All installed packages land here
└── pyvenv.cfg    # Points to the managed Python interpreter

Key difference from venv: uv’s .venv/ is created in milliseconds, not seconds. And uv run means you never need to source .venv/bin/activate.

uv.lock — The Cross-Platform Lockfile

The uv.lock file is created when you first run uv sync or uv lock. This is a complete, resolved dependency graph — every package pinned to an exact version, with the hashes to verify integrity.

[[package]]
name = "fastapi"
version = "0.115.0"
source = { registry = "https://pypi.org/simple" }
sdist = { hash = "sha256:abc123..." }
wheel = [{ url = "...", hash = "sha256:def456..." }]

Always commit uv.lock to version control. When your colleague clones the repo and runs uv sync, they get bit-for-bit identical environments — across macOS, Windows, and Linux. This is the end of “works on my machine.”

Lockfile vs requirements.txt: A requirements.txt pins top-level packages only. uv.lock locks every single transitive dependency — sub-dependencies, sub-sub-dependencies, and all.

Running Your First Script — uv run main.py

uv’s killer convenience: you never manually manage environments. On first run, it creates .venv/, syncs dependencies, and executes — all in one command.

uv run main.py
Hello from my-project!
Python 3.12.3

On a fresh machine with no .venv/ present, this command:

  • Reads .python-version → installs Python 3.12 if missing
  • Creates .venv/
  • Runs uv lock to resolve and pin dependencies
  • Runs uv sync to install everything
  • Executes main.py inside the environment

All of that happens automatically. You just wait for the output.

Managing Python Versions

uv bundles its own Python distribution management. No pyenv, no python-build, no manual downloads.

Installing a New Python Version

# Install Python 3.12 (if not already present)
uv python install 3.12

# Install multiple versions at once
uv python install 3.11 3.12 3.13

Listing Installed Versions

uv python list
cpython-3.12.3+freethreaded-linux-x86_64-gnu    <-- currently pinned
cpython-3.13.1-linux-x86_64-gnu
cpython-3.11.9-linux-x86_64-gnu

Switching Between Versions

# Pin a specific version for this project
uv python pin 3.11

# Verify .python-version was updated
cat .python-version
3.11

Automatic discovery: If .python-version says 3.12 but only 3.11 is installed, uv sync prompts you to install the right version. No manual intervention needed.

Migrating Existing Projects

You have a project with a requirements.txt. Migrating takes two commands.

From requirements.txt

# Navigate to your existing project
cd my-old-project

# Run uv init (preserves existing files in the directory)
uv init

# Import all packages from requirements.txt into pyproject.toml
uv add -r requirements.txt

This reads requirements.txt, resolves every version, writes uv.lock, and installs everything — all in one step.

From Poetry

If your project uses Poetry, export your dependencies and import them with uv:

uv init
poetry export -f requirements.txt --without-hashes -o /tmp/req.txt
uv add -r /tmp/req.txt

Tip: The official Astral migration guide covers Poetry, pip-tools, pipenv, and conda — with exact commands for each.

Practical Mini-Project: A FastAPI App with uv

Let us tie everything together. You will build a real FastAPI application using only uv commands — from scaffold to running server.

Step 1: Scaffold the Project

uv init tasks-api
cd tasks-api

Step 2: Add FastAPI and Uvicorn

# Add FastAPI as a runtime dependency
uv add fastapi uvicorn

# Add pytest as a dev dependency
uv add --dev pytest httpx

Check pyproject.toml now:

[project]
name = "tasks-api"
version = "0.1.0"
description = "A simple task API built with FastAPI"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "fastapi>=0.115.0",
    "uvicorn>=0.32.0",
]

[dependency-groups]
dev = [
    "pytest>=8.0.0",
    "httpx>=0.28.0",
]

Step 3: Write the API

Replace main.py with this:

from fastapi import FastAPI, HTTPException

app = FastAPI(title="Tasks API", version="0.1.0")

tasks = []


@app.get("/")
def root():
    return {"message": "Tasks API is running", "task_count": len(tasks)}


@app.get("/tasks")
def list_tasks():
    return tasks


@app.post("/tasks")
def create_task(task: dict):
    tasks.append(task)
    return {"added": task}


@app.delete("/tasks/{task_id}")
def delete_task(task_id: int):
    if 0 <= task_id < len(tasks):
        removed = tasks.pop(task_id)
        return {"deleted": removed}
    raise HTTPException(status_code=404, detail="Task not found")

Step 4: Run the Server

uv run uvicorn main:app --reload --port 8000
INFO:     Uvicorn running on http://127.0.0.1:8000
INFO:     Application startup complete.

Step 5: Test It

curl -X POST http://127.0.0.1:8000/tasks 
  -H "Content-Type: application/json" 
  -d '{"title": "Finish uv tutorial", "priority": "high"}'
{"added": {"title": "Finish uv tutorial", "priority": "high"}}
curl http://127.0.0.1:8000/tasks
[{"title": "Finish uv tutorial", "priority": "high"}]

Add a tests/test_tasks.py file to make testing reproducible:

import pytest
from httpx import AsyncClient, ASGITransport
from main import app


@pytest.mark.asyncio
async def test_create_and_list_task():
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.post("/tasks", json={"title": "Test task"})
        assert response.status_code == 200
        assert response.json()["added"]["title"] == "Test task"

        response = await client.get("/tasks")
        assert response.status_code == 200
        assert len(response.json()) == 1
uv run pytest tests/test_tasks.py

Run uv run pytest whenever you add or change endpoints — it uses the same environment uv sync set up, so no manual activation needed.

Step 6: Share the Project

Your collaborator clones the repo and runs one command:

uv sync       # Reads uv.lock, creates .venv, installs everything
uv run uvicorn main:app --reload

Zero manual setup. Exact same versions. Works identically on their machine.

Common Mistakes and Gotchas

  • Do not commit .venv/ to git. It is in .gitignore by default — keep it that way.
  • Do not edit uv.lock manually. It is machine-generated. Edit pyproject.toml instead and run uv lock --upgrade.
  • uv run vs uv sync: uv run is for executing scripts. uv sync is for setting up an environment for development or deployment.
  • Python version mismatch: If you push .python-version = 3.13 but your CI only has 3.12, the run will fail. Always run uv python install in CI before uv sync.

Summary and Next Steps

In this tutorial you learned how uv scaffolds a complete Python project with four files, manages Python versions without pyenv, runs scripts without environment activation, and locks every dependency for reproducible environments.

You bootstrapped a working FastAPI app in under ten commands — and it will work identically on any machine that clones the repo. If you are deciding between FastAPI, Flask, and Django for your next API project, here is a full comparison of Python API frameworks to help you choose.

To go further with uv:

Related posts
ProgrammingPython

Production-Ready MCP Servers — Security, Testing & Deployment

ProgrammingPython

Build Your First MCP Server with Python SDK — Fundamentals

ProgrammingPython

Connect FastAPI to MCP — Two Integration Patterns

ProgrammingPython

Replace pip with uv for Faster Python Development

Leave a Reply