Everyone is touting uv's speed (UV: A Python Package Management Powerhouse - 100x Faster than pip), but for our lab’s need to migrate Python environments across multiple machines, uv’s ease of environment reproducibility is equally worth highlighting.
Although we’ve been actively recommending uv to colleagues, many still deeply rely on conda and are reluctant to try new tools. Therefore, I’ve decided to write this tutorial introducing uv’s usage, hoping to gently push forward a shift in how we manage lab environments.
On Windows, I still recommend using winget:
On Linux/macOS, refer to the official documentation using curl or wget:
If you need to initialize a new project, run the following in your workspace directory:
Or, if you want to migrate an existing Python project into a uv environment, run:
This will create the following files in your project directory:
.git, .gitignore, and README.md need no explanation.main.py is just a sample Python file—delete it if not needed.pyproject.toml is the configuration file for Python projects, storing metadata and dependencies. Its content looks like this:
toml
[project]
name = "test-project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
.python-version specifies the Python version for the current project.Edit the requires-python field in pyproject.toml to specify the required Python version for your project, e.g., ==3.12, >=3.12, etc.
Then run the following command to install the specified Python version:
This command creates a virtual environment in the current project directory, named .venv. Afterward, follow the output instructions to activate the environment, such as:
To install a new dependency, use:
It’s essentially replacing pip install with uv add. However, this command not only installs the dependency into the virtual environment but also automatically writes the dependency information into the dependencies field in pyproject.toml, and updates the uv.lock lock file to ensure reproducible dependency versions. The uv.lock file should never be edited manually, but it is crucial for environment reproducibility—do not add it to .gitignore.
For example, after installing requests, pyproject.toml will only have one additional line: requests. Meanwhile, uv.lock records detailed version information for requests and all its sub-dependencies. Use the uv tree command to view the dependency tree:
If you no longer need a dependency, remove it using:
To switch to a different PyPI mirror, add the following to pyproject.toml (using Nanjing University’s mirror as an example):
Some packages are published on their own PyPI mirrors—for example, the pip installation command for torch (see PyTorch Official Documentation):
To use uv with PyTorch, refer to Using uv with PyTorch and add the following to pyproject.toml:
Otherwise, it will install the CPU version instead of the CUDA-enabled version of torch and torchvision.
First, define an index source named pytorch-cu130, with url set to the --index-url from the official document, though here I’ve replaced it with the Nanjing University mirror: https://mirror.nju.edu.cn/pytorch/whl/cu130. Then, under tool.uv.sources, specify that torch and torchvision must be installed from this index (note: marker ensures this applies only when the OS is Linux or Windows; macOS users will ignore this config and use the default source).
When you need to reproduce the Python environment on another machine, simply clone the project code and run:
To reproduce the same environment in another project, copy the pyproject.toml, uv.lock, and .python-version files into the target project directory, update the project info in pyproject.toml, then run the same command:
If you’re developing a package for others to install, you’ll need to distinguish between development and runtime dependencies. Install development dependencies using uv add --dev [package-name].
You’ll also need to complete the project metadata in pyproject.toml, such as author, license, and build information. Since most research work doesn’t involve publishing packages, we won’t go into detail here—but you can refer to the following example for understanding:
After that, you can build and publish your package using:
uv cache clear: Clear uv cache to free up disk spaceuv tree: View the current project’s dependency treeuv run main.py: Equivalent to activating the virtual environment and running python main.py; useful for running Python scripts in your projectuvx [script-name]: Run a tool. Some Python packages provide command-line tools—use uvx to run them, e.g., uvx NJUlogin -h.winget install astral.uv
curl -LsSf https://astral.sh/uv/install.sh | sh
# or
wget -qO- https://astral.sh/uv/install.sh | sh
uv init [project-name]
cd [project-name]
cd [project-name]
uv init
.gitignore
main.py
pyproject.toml
.python-version
README.md
uv python pin 3.12
uv venv
source .venv/bin/activate
# or on Windows
.venv\Scripts\activate
uv add [package-name]
> uv tree
Resolved 6 packages in 0.75ms
test-project v0.1.0
└── requests v2.32.5
├── certifi v2025.11.12
├── charset-normalizer v3.4.4
├── idna v3.11
└── urllib3 v2.6.1
uv remove [package-name]
uv pip is available for compatibility with pip, it does not update pyproject.toml or uv.lock, leading to unreproducible environments.[[tool.uv.index]]
name = "nju-mirror"
url = "https://mirror.nju.edu.cn/pypi/web/simple"
default = true
pip3 install torch torchvision --index-url https://download.pytorch.org/whl/cu130
[[tool.uv.index]]
name = "pytorch-cu130"
url = "https://mirror.nju.edu.cn/pytorch/whl/cu130"
explicit = true
[tool.uv.sources]
torch = [
{ index = "pytorch-cu130", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
]
torchvision = [
{ index = "pytorch-cu130", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
]
uv sync
uv sync
[project]
name = "NJUlogin"
version = "3.6.1"
description = "The Nanjing University login module, which can be used to login to the various campus web sites"
authors = [{ name = "Do1e", email = "i@do1e.cn" }]
readme = "README.md"
requires-python = ">=3.10,<4.0"
dependencies = [
"requests>=2.32.0",
"pillow>=11.0.0",
"numpy>=2.0.0",
"lxml>=5.3.0",
"pycryptodome>=3.21.0",
"onnxruntime>=1.20.0",
"cryptography>=43.0.0",
]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
[project.scripts]
NJUlogin = "NJUlogin.__main__:main"
[tool.setuptools.packages.find]
include = ["NJUlogin", "NJUlogin.*"]
[project.urls]
Homepage = "https://github.com/Do1e/NJUlogin"
Repository = "https://github.com/Do1e/NJUlogin"
[dependency-groups]
dev = [
"pre-commit>=4.3.0",
"ruff>=0.14.6",
]
[[tool.uv.index]]
url = "https://mirror.nju.edu.cn/pypi/web/simple"
publish-url = "https://upload.pypi.org/legacy/"
default = true
[tool.ruff]
line-length = 100
[tool.ruff.lint]
ignore = ["C901", "E501", "E721", "E741", "F402", "F823"]
select = ["C", "E", "F", "I", "W"]
[tool.ruff.lint.isort]
lines-after-imports = 2
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.uv]
package = true
uv build
uv publish