Programming2025-12-10

Setting Up a Python Project with uv

The fastest and most reliable way to initialize a clean, reproducible Python project using uv, the Rust-based package manager. Covers project scaffolding, dependency management, and inline scripting for single-file tools.

#Python#Programming#Tooling

Overview

This guide outlines the standard process for initializing a clean, reproducible Python project using uv, the Rust-based package manager. It is optimized for open source projects where reproducibility, minimal setup friction, and a clean dependency graph matter.

uv replaces pip, venv, pip-tools, and pyenv with a single tool that is significantly faster than any of them.

Why uv

Before uv, a typical Python project setup required:

  1. Installing the right Python version manually or via pyenv
  2. Creating a virtual environment with python -m venv .venv
  3. Activating it with source .venv/bin/activate
  4. Installing packages with pip install
  5. Freezing dependencies into requirements.txt

This process is fragile, slow, and produces lockfiles that are not platform-aware. uv replaces all of it with a single tool backed by a Rust resolver that handles environment creation, dependency locking, and script execution automatically.

Step 1: Scaffold the Project

Instead of manually creating directories and virtual environments, let uv handle the boilerplate.

code
uv init my_project
cd my_project

This single command:

  • Creates the my_project/ directory
  • Generates a pyproject.toml with project metadata
  • Creates a hello.py entry point
  • Pins the current Python version in .python-version

The resulting structure is immediately ready for development and compatible with PyPI publishing conventions.

Step 2: Add Dependencies

Never use pip install in a uv-managed project. Always use uv add to ensure every dependency is tracked in pyproject.toml and reflected in the lockfile.

code
# Add a standard runtime package
uv add requests

# Add a development dependency (formatters, linters, test runners)
uv add --dev ruff
uv add --dev pytest

Running uv add does three things atomically:

  1. Creates .venv/ if it does not already exist
  2. Installs the package into the environment
  3. Updates uv.lock with a pinned, platform-aware lockfile

The uv.lock file should be committed to version control. It guarantees that every contributor and every CI run installs the exact same dependency tree regardless of when the install happens.

Step 3: Run the Project

You do not need to activate the virtual environment manually. uv run handles the context automatically.

code
uv run hello.py

This resolves the environment from pyproject.toml, ensures all dependencies are installed, and executes the script in the correct context. There is no source .venv/bin/activate step.

For projects with a defined entry point, you can also run tools directly:

code
uv run pytest
uv run ruff check .

Step 4: Sync a Cloned Project

When a contributor clones the repository, they only need one command to get a fully working environment:

code
uv sync

This reads uv.lock, creates .venv/, and installs every dependency at the pinned versions. No manual steps required. This is the key advantage of uv over a plain requirements.txt workflow for open source projects.

Recommended pyproject.toml Structure

A clean starting point for an open source project:

code
[project]
name = "my-project"
version = "0.1.0"
description = "A short description of what this does."
readme = "README.md"
requires-python = ">=3.11"
license = { text = "MIT" }
authors = [{ name = "Your Name", email = "[email protected]" }]

dependencies = [
    "requests>=2.32",
]

[project.urls]
Repository = "https://github.com/yourname/my-project"

[tool.uv]
dev-dependencies = [
    "ruff>=0.4",
    "pytest>=8.0",
]

Inline Scripting for Single-File Tools

For single-file scripts where a full project structure is unnecessary, use uv's inline script metadata format. This is useful for quick red team tools, automation scripts, or one-off utilities.

Add a metadata block to the top of the script file:

code
# /// script
# requires-python = ">=3.11"
# dependencies = [
#     "requests",
#     "colorama",
# ]
# ///

import requests
import colorama

print("Running...")

Then execute it directly from anywhere on the system:

code
uv run script.py

uv reads the metadata comment block, creates an ephemeral environment in the cache, installs the listed dependencies, runs the script, and discards the environment. No requirements.txt, no venv, no activation step.

This format is defined by PEP 723 and is the correct way to distribute portable Python scripts that have dependencies.

Common Commands Reference

code
# Initialize a new project
uv init my_project

# Add a runtime dependency
uv add requests

# Add a development dependency
uv add --dev pytest

# Run a script or tool
uv run hello.py
uv run pytest

# Sync environment from lockfile (after cloning)
uv sync

# Update all dependencies to their latest compatible versions
uv lock --upgrade

# Remove a dependency
uv remove requests

# Show installed packages
uv pip list

# Run an inline script with no project setup
uv run script.py

Project Structure After Setup

code
my_project/
├── .python-version     # Pinned Python version
├── .venv/              # Managed automatically by uv
├── hello.py            # Entry point
├── pyproject.toml      # Project metadata and dependencies
├── uv.lock             # Deterministic lockfile (commit this)
└── README.md

The .venv/ directory should be added to .gitignore. Everything else, including uv.lock, should be committed.