uv is a fast, modern tool for managing Python projects, designed to handle dependency management, packaging, and environment isolation in a unified way. Unlike traditional tools that require separate configurations, uv uses a single pyproject.toml file for project specifications, making it easier to manage dependencies, build projects, and ensure reproducible environments across different machines.
Benefits of Using uv Over Traditional Tools¶
Exceptional Speed: uv is written in Rust and can be 10-100x faster than traditional tools like pip for dependency resolution and installation.
Unified Ecosystem: Replaces multiple tools (pip, pip-tools, poetry, pyenv, virtualenv, etc.) with a single, cohesive solution.
Simplified Dependency Management: Efficiently resolves dependencies and avoids common issues like dependency conflicts.
Universal Lockfile: The
uv.lockfile ensures reproducible builds across different platforms (macOS, Linux, Windows) without needing separate lockfiles.Built-in Python Management: Easily install and switch between Python versions directly within uv.
Easy Version Management: Specify version constraints for dependencies and handle updates smoothly.
Intuitive CLI: Offers a user-friendly command-line interface for managing projects, dependencies, and environments.
Installing uv and Basic Setup¶
To install uv, run the following command in your terminal:
curl -LsSf https://astral.sh/uv/install.sh | shThis installs uv as a standalone tool. You can verify the installation by running uv --version.
Initialising a New Project with uv¶
uv supports two main project types: applications and packages. Understanding the difference between them is key to choosing the right starting point.
Applications¶
An application is the default project type in uv — suitable for scripts, web servers, and command-line tools. Think of it as code you run, not code you import.
To create a new application project, run:
uv init my-app
cd my-appInitialized project `my-app` at `/home/runner/work/msdp-book/home/ch41/my-app`
This command creates a new my-app directory with the following files and directories:
my-app/
├── .gitignore ] Git ignore file (starts with common Python ignores)
├── .git/ ] Git repository (initialized by uv)
├── .python-version ] Python version specification for uv
├── README.md ] Project documentation (starts empty)
├── main.py ] Default executable entry point
└── pyproject.toml ] Project metadata and dependency manifestTo make the scaffold concrete, here is the default content of each file and why it matters.
.python-version
This file contains the Python version that is pinned for the project. When you run uv sync or uv run, uv will automatically create a virtual environment with that Python version and install your dependencies there. This ensures that your project is isolated from the system Python and other projects, and that everyone working on the project uses the same Python version.
If we look inside the file, we see the Python version:
3.12
README.md
This starts empty on purpose. You are expected to fill it with the project goal, setup steps, and run instructions.
main.py
This is the default executable entry point. The main() function contains your application logic, and the if __name__ == "__main__": block runs it when the file is executed as a script.
def main():
print("Hello from my-app!")
if __name__ == "__main__":
main()pyproject.toml
This is the project’s metadata and dependency manifest. It is the central configuration file for your project, where you declare the project name, version, description, Python version requirements, and dependencies. The pyproject.toml file is used by uv to manage dependencies and build the project. The default content is:
nameandversion: identify the project.descriptionandreadme: provide package metadata.requires-python: declares the supported Python versions.dependencies: starts empty and is populated as you add libraries.
If you open the pyproject.toml file, you will see the following content:
[project]
name = "my-app"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
At this point, the project is ready to run:
uv run main.pywarning: `VIRTUAL_ENV=/home/runner/work/msdp-book/msdp-book/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
Using CPython 3.12.3 interpreter at: /usr/bin/python3.12
Creating virtual environment at: .venv
Hello from my-app!
Before moving to larger project layouts, it is useful to clarify the terminology used in this chapter.
Project Structure¶
As your project grows, you may want to split your code into multiple reusable modules. This is when you need an import package: a directory of .py files with an __init__.py that makes them importable within your project. Two layouts are commonly used in the Python ecosystem.
Flat Layout¶
In the flat layout, the import package directory sits directly in the root of the project:
my-app/
├── pyproject.toml ] Project metadata and build configuration
├── my_project/ ┐
│ ├── __init__.py │ Package source code
│ ├── moduleA.py │
│ └── moduleB.py ┘
└── tests/ ┐
└── test_file1.py | Package tests
└── .... ┘This layout has a long history in the Python ecosystem for creating reusable import packages. Many of the most widely-used scientific Python packages use the flat layout — including NumPy, SciPy, pandas, xarray, scikit-learn, and Jupyter. Migrating these large, established codebases to a different layout would require considerable effort, so they continue to use the flat layout.
uv does not generate this structure directly. After running uv init, you would manually create the package directory and an __init__.py file, then add a build system to pyproject.toml to make the package installable.
One drawback of the flat layout is that when running tests from the project root, Python may inadvertently import the local source files directly rather than the installed version of the package. This can hide packaging problems that users would encounter when installing your package.
src Layout¶
The src layout is specifically designed for creating distribution packages — code you want to share and install via package managers like PyPI. The package directory is placed inside a src/ subdirectory. uv creates this structure automatically when you use the --package flag:
uv init --package my-package
cd my-packageInitialized project `my-package` at `/home/runner/work/msdp-book/home/ch41/my-package`
The resulting structure is:
my-package/
├── .python-version
├── README.md
├── pyproject.toml
└── src/
└── my_package/
└── __init__.pyUnlike the plain application template, this pyproject.toml includes a build system — this tells uv that the code is set up as a distribution package (i.e., ready to be packaged and distributed). When you run uv sync, uv builds and installs the distribution package into the virtual environment in editable (development) mode:
[project]
name = "my-package"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[project.scripts]
my-package = "my_package:main"
[build-system]
requires = ["uv_build>=0.11.14,<0.12.0"]
build-backend = "uv_build"
The exact uv_build version range may differ depending on your installed uv version.
Compared with the application version, two sections are added:
[project.scripts]: defines a command-line entry point. Here, runningmy-packageexecutesmy_package:main.[build-system]: marks the project as a distribution package and tells build tools how to build/install it.
The core [project] metadata fields are the same as in the application template.
With the src layout, the package directory is isolated from the project root. This, combined with the package being installed via uv sync, ensures you’re always importing the installed version during development. This approach catches packaging issues early, rather than after distribution to users.
The src layout is recommended by PyPA and pyOpenSci for new packages.
About cases and case conventions:¶
In the course, we will use:
kebab case for the project name (and use it also for the remote repository name),
snake case for the import package name (convert kebab-case project names to snake_case by replacing dashes with underscores).
Since valid Python identifiers cannot contain dashes (-), we should avoid using dashes in the import package names.
A valid identifier cannot start with a number, or contain any spaces and can only contain alphanumeric letters (a-z) and (0-9), or underscores (_).
Converting a Pre-existing Project to Use uv¶
Sometimes we don’t want to start from scratch but rather convert an existing project to use uv. To convert an existing project to uv, navigate to the project’s root directory and run:
uv initThis command will create a pyproject.toml file if one doesn’t exist, and you can then add dependencies using uv add <package>. If you have existing requirements files, you can import them with uv add -r requirements.txt. This allows you to migrate your project to uv’s modern dependency management while preserving your existing code.