2 Python projects with uv
Learn how to use uv to create, manage, and maintain Python projects. Understand virtual environments, dependency management, and the modern Python packaging ecosystem.
2.1 Why virtual environments
In Python, virtual environments are not optional. They are essential for any serious project work.
Unlike R’s renv (which primarily helps with reproducibility), Python virtual environments serve a fundamental purpose: isolating project dependencies from the system Python.
Here is why this matters:
- Different projects need different package versions.
- System Python library should never be modified directly.
- Dependency conflicts are common and destructive.
- Reproducibility requires exact version control.
Installing packages globally with pip install without a virtual environment will cause conflicts and break system tools. Always use virtual environments. To install Python packages as global command-line tools, use pipx.
2.2 What is uv
uv is a modern Python package and project manager written in Rust. It replaces and improves upon a scattered toolchain:
pip(package installation)venv(virtual environment creation)pyenv(Python version management)pip-tools(dependency locking)setuptools(package building)
Benefits of uv:
- Fast: 10-100x faster than pip due to Rust implementation.
- Complete: Manages Python versions, dependencies, and builds.
- Modern: Uses
pyproject.tomlas the single source of truth. - Reliable: Automatic dependency resolution and lock files.
In R terms, uv combines functionality from renv, devtools, usethis, and pak into a single, cohesive tool.
2.3 Python packaging standards
Python has standardized on pyproject.toml as the configuration file for all projects. This is similar to R’s DESCRIPTION file but uses TOML format.
The official Python packaging guide is available at https://packaging.python.org/.
Key concepts:
pyproject.tomldefines project metadata and dependencies.uv.lockrecords exact versions (likerenv.lock).- Build backends (like
hatchling) create distributable packages.
2.4 Installing uv
Follow the official installation guide.
macOS and Linux:
curl -LsSf https://astral.sh/uv/install.sh | shWindows:
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"Via Homebrew (macOS):
brew install uvVerify installation:
uv --version2.5 Updating uv
uv can update itself:
uv self updateRegular updates are important because uv frequently adds support for new Python versions and features.
uv uses Python distributions from the python-build-standalone project. These are optimized, portable Python builds that work consistently across platforms.
2.6 Initialize a project
If you are using GitHub Codespaces, the pycsr project folder is opened by default. Before creating a new practice project, close this folder via File > Close Folder so your shell returns to the home directory. This prevents uv from getting confused about which uv.lock file to write when you “initialize a new project within an existing project”.
Create a new Python project:
uv init pycsr-example
cd pycsr-exampleThis creates a basic structure:
pycsr-example/
├── .python-version # Pinned Python version
├── pyproject.toml # Project metadata and dependencies
├── README.md # Project documentation
└── src/
└── pycsr_example/
└── __init__.py
Notice the directory name uses hyphens (pycsr-example) while the package name uses underscores (pycsr_example). This is Python convention.
2.6.1 Project structure
The pyproject.toml file contains project configuration:
[project]
name = "pycsr-example"
version = "0.1.0"
description = "Example clinical study report project"
dependencies = []
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"Key sections:
[project]: Package metadata.[project.dependencies]: Hard, runtime dependencies.[dependency-groups.dev]: Development dependencies.[build-system]: How to build the package.
2.7 Pin Python version
Specify the exact Python version for your project:
uv python pin 3.14.0This updates .python-version file so everyone uses the same Python version when they restore the environment.
Use the full MAJOR.MINOR.PATCH version (for example, 3.14.0) rather than just MAJOR.MINOR (for example, 3.14). This prevents drift as new patch versions are released.
Why pin the exact version:
- Patch releases can introduce subtle behavior changes.
- Reproducibility requires exact version matching.
- Regulatory submissions should document the exact Python version.
Check which Python versions are available:
uv python listInstall a specific Python version if needed:
uv python install 3.14.02.8 Managing dependencies
2.8.1 Adding dependencies
Add runtime dependencies:
uv add polars plotnine rtfliteAdd development-only dependencies:
uv add --dev ruff pytest mypyThis updates pyproject.toml:
[project]
dependencies = [
"plotnine>=0.15.1",
"polars>=1.35.2",
"rtflite>=1.1.0",
]
[dependency-groups]
dev = [
"mypy>=1.18.2",
"pytest>=9.0.1",
"ruff>=0.14.5",
]By default, uv adds dependencies with >= constraints. This allows updates within compatible versions. The lock file ensures exact versions are used.
2.8.2 Removing dependencies
Remove a package:
uv remove pandasThis removes the package from both pyproject.toml and the environment.
2.9 Lock files and syncing
2.9.1 Creating and updating the lock file
Generate or update the lock file:
uv syncThis creates uv.lock, which records:
- Exact version of every package.
- All transitive dependencies.
- Package hashes for verification.
The lock file ensures reproducibility across different machines and over time.
2.9.2 Upgrading dependencies
To update packages while respecting constraints in pyproject.toml:
uv lock --upgradeThen synchronize the environment:
uv syncThis is similar to:
- R:
renv::update()followed byrenv::snapshot(). - Node.js:
npm updatefollowed bynpm install.
The two-step process (lock & sync) gives you control: you can review lock file changes before updating your environment.
2.10 Running commands
You have two options for running commands in your project environment.
2.10.1 Option 1: Activate the virtual environment
source .venv/bin/activate # macOS/Linux
# or
.venv\Scripts\activate # WindowsThen run commands directly:
python -m pycsr_example
pytest
ruff checkDeactivate when done:
deactivate2.10.2 Option 2: Use uv run
Run commands without activation:
uv run python -m pycsr_example
uv run pytest
uv run ruff checkuv run is convenient for one-off commands and CI/CD scripts. For interactive work, activating the environment is often more ergonomic.
2.10.3 uv run and uvx
uvx runs tools in isolated, temporary environments:
uvx ruff check .
uvx black --check .Use uvx when:
- Running tools you don’t want to install in the project.
- Trying packages without adding them as dependencies.
- Running scripts that declare their own dependencies.
Use uv run when:
- Running project code.
- Running tests.
- Using project dependencies.
See using tools in uv for details.
2.11 Building and publishing
For creating distributable packages, you need a build backend. The simplest option is hatchling.
Add to pyproject.toml:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"2.11.1 Build wheel
Create distribution files:
uv buildThis creates:
dist/pycsr_example-0.1.0.tar.gz(source distribution)dist/pycsr_example-0.1.0-py3-none-any.whl(wheel)
2.11.2 Publish to PyPI
Publish to the Python Package Index:
uv publishBuilding and publishing are not typically needed for internal clinical reporting projects. However, if you develop reusable tools like table generation packages, open sourcing in a GitHub repository and publishing on PyPI will make them more visible.
2.12 Exercise
Create a small project to practice uv commands:
- Initialize a new project called
csr-practice. - Pin Python to version 3.14.0 (or latest available).
- Add
polarsas a dependency. - Add
pytestas a development dependency. - Examine the generated
pyproject.tomlanduv.lockfiles. - Run Python using
uv run python --version.
View solution
# Initialize project
uv init csr-practice
cd csr-practice
# Pin Python version
uv python pin 3.14.0
# Add dependencies
uv add polars
uv add --dev pytest
# View configuration
cat pyproject.toml
# Check lock file
cat uv.lock
# Run Python
uv run python --versionYour pyproject.toml should look similar to:
[project]
name = "csr-practice"
version = "0.1.0"
description = "Add your description here"
dependencies = [
"polars>=1.18.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.3.4",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"2.13 What’s next
Now that you understand uv basics, the next chapter covers the Python package toolchain:
- Formatting and linting with Ruff.
- Type checking with mypy.
- Testing with pytest.
- Documentation generation.
- Development workflows for clinical reporting.