⥠UV â Modern Python Package & Environment Management¶
Why this doc exists: Previous projects at 42 used plain
venv+pip. This project uses UV â a faster, more complete replacement.
đ¯ Purpose¶
This guide focuses on:
- What UV is
- Why UV is becoming popular
- How UV compares to venv
- Understanding
pyproject.toml - Understanding
uv.lock - Typical UV workflows
- Using UV through a Makefile
Table of Contents¶
- What is UV?
- Why not just use venv + pip?
- Installing UV
- Key Concepts
- pyproject.toml
- uv.lock
- Getting Started with this Project
- Common UV Commands
- UV vs venv + pip â Side by Side
- Using the Makefile
- Project Structure
- FAQ
What is UV?¶
UV is a Python package and environment manager built by Astral (the same team behind the ruff linter). It's written in Rust, which is a big part of why it's so fast.
Think of UV as a single tool that replaces:
| Old Tool | UV Equivalent |
|---|---|
python -m venv |
uv venv |
pip install |
uv add / uv sync |
pip freeze |
automatic via uv.lock |
pip-tools / pip-compile |
built-in |
The goal: one tool, reproducible environments, no surprises.
Why not just use venv + pip?¶
If you've worked on other 42 Python projects, you're probably used to this flow:
python3 -m venv venv
source venv/bin/activate
pip install requests
pip freeze > requirements.txt
This works for small solo scripts. But it has real problems in team or multi-machine scenarios.
The "works on my machine" problem¶
Imagine this:
You add `requests` to your project in January.
A friend clones the repo in April.
pip installs requests==2.32.0 for you, requests==2.33.1 for them.
Subtle differences in behaviour. Debugging nightmare.
With UV and a lock file, both of you get exactly the same versions, every time.
pip is slow¶
UV resolves and installs packages significantly faster than pip. On a fresh install of a project with many dependencies, the difference is very noticeable.
requirements.txt is fragile¶
pip freeze dumps every installed package â including things that got pulled in indirectly. The file isn't structured, doesn't track dev vs runtime deps, and drifts over time.
UV uses pyproject.toml + uv.lock instead â structured, reliable, and version-controlled properly.
Installing UV¶
Linux / macOS¶
curl -LsSf https://astral.sh/uv/install.sh | sh
Then restart your terminal (or source ~/.bashrc / source ~/.zshrc) and verify:
uv --version
# uv 0.x.x (...)
Windows¶
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
42 School machines (Linux): The
curlcommand above is the one to use. UV installs to~/.local/bin/by default, which should already be in your PATH.
Key Concepts¶
pyproject.toml¶
This is the declaration of your project. It's the single source of truth for:
- Project name and version
- Runtime dependencies (things the code needs to run)
- Dev dependencies (things you need during development, like test runners)
- Python version constraints
Example from this project:
[project]
name = "call-me-maybe"
version = "0.1.0"
description = "42 Python project â socket programming"
requires-python = ">=3.10"
dependencies = [
"requests>=2.28.0",
]
[tool.uv]
dev-dependencies = [
"pytest>=7.0",
]
You edit pyproject.toml directly, or let UV update it when you run uv add. Think of it like a contract: "this project needs at least these things."
uv.lock¶
This is the resolved snapshot of your environment. UV generates and updates it automatically â you never edit it by hand.
It records the exact versions of every package (including transitive dependencies â packages that your packages depend on):
# Example excerpt from uv.lock
[[package]]
name = "requests"
version = "2.32.3"
source = { registry = "https://pypi.org/simple" }
[[package]]
name = "urllib3"
version = "2.5.0"
[[package]]
name = "certifi"
version = "2026.01.01"
Why does this matter?¶
pyproject.toml says: "requests>=2.28.0" â flexible requirement
uv.lock says: "requests==2.32.3" â exact pinned version
When you run uv sync, UV reads the lock file and installs those exact versions â not whatever happens to be latest today. This means:
- You, your teammate, and the CI pipeline all get identical environments
- No more debugging version-related differences across machines
- Rolling back is easy â just revert the lock file in git
Both pyproject.toml and uv.lock should be committed to git.
Getting Started with this Project¶
Clone the repo and set up your environment in three commands:
git clone <repo-url>
cd call-me-maybe
uv sync
That's it. UV will:
1. Read pyproject.toml and uv.lock
2. Create a .venv/ directory
3. Install all dependencies at the exact pinned versions
Then run the project:
uv run python main.py
Or run tests:
uv run pytest
Note: You don't need to activate the virtual environment manually.
uv runhandles that for you. If you prefer the traditionalsource .venv/bin/activate, that still works too.
Common UV Commands¶
Adding a dependency¶
uv add requests
UV will:
- Install the package
- Add it to pyproject.toml under [project] dependencies
- Update uv.lock with the resolved versions
Adding a dev-only dependency (e.g. a testing library):
uv add --dev pytest
Removing a dependency¶
uv remove requests
Syncing your environment¶
After pulling new changes from git (someone else may have added dependencies):
uv sync
This reads the lock file and makes your local environment match it exactly. It's the equivalent of pip install -r requirements.txt, but more reliable.
Running code¶
uv run python main.py
uv run pytest
uv run python -c "import sys; print(sys.version)"
Upgrading a dependency¶
uv add requests --upgrade
This upgrades requests to the latest compatible version and updates uv.lock.
Updating all dependencies¶
uv lock --upgrade
uv sync
Checking what's installed¶
uv pip list
UV vs venv + pip â Side by Side¶
Here's the same task done both ways:
Setting up a fresh project¶
Old way (venv + pip):
python3 -m venv venv
source venv/bin/activate
pip install requests pytest
pip freeze > requirements.txt
# requirements.txt now has ~20 packages, including ones you didn't ask for
UV way:
uv init my_project
cd my_project
uv add requests
uv add --dev pytest
# pyproject.toml is clean, uv.lock has the full resolution
Cloning someone else's project¶
Old way:
git clone <repo>
cd repo
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
# Hope the versions in requirements.txt are still installable...
UV way:
git clone <repo>
cd repo
uv sync
# Done. Exact same environment as everyone else.
Feature comparison¶
| Feature | venv + pip | UV |
|---|---|---|
| Virtual environment | â | â |
| Package installation | â | â |
| Lock file | â (manual with pip-tools) | â automatic |
| Reproducible installs | â ī¸ limited | â guaranteed |
| Dev vs runtime deps | â manual convention | â built-in |
| Speed | normal | very fast (Rust) |
| Single tool | â | â |
| Works without activating venv | â | â
(via uv run) |
Using the Makefile¶
The Makefile in this project is just a shortcut layer on top of UV commands. You don't need it, but it makes the most common tasks quick to type:
install:
uv sync
run:
uv run python main.py
test:
uv run pytest
clean:
rm -rf .venv __pycache__ .pytest_cache
Usage:
make install # set up / sync the environment
make run # run the project
make test # run tests
make clean # remove generated files
Project Structure¶
call-me-maybe/
âââ .venv/ â created by UV, do NOT commit (in .gitignore)
âââ src/
â âââ *.py
âââ tests/
â âââ test_*.py
âââ docs/
â âââ UV_GUIDE.md â you are here
âââ pyproject.toml â commit this â
âââ uv.lock â commit this â
âââ Makefile
âââ README.md
.venv/is in.gitignore. Each developer generates their own local environment viauv sync. You never commit the virtual environment itself.
FAQ¶
Q: Do I still need to activate the virtual environment?
You don't have to. uv run <command> automatically uses the project's .venv. If you prefer the traditional workflow, source .venv/bin/activate still works exactly as before.
Q: Someone added a new package. How do I get it?
git pull
uv sync
UV will detect that uv.lock changed and install the new package.
Q: Can I still use pip inside the project?
Technically yes (if you activate .venv first), but you shouldn't â pip won't update uv.lock, so your changes won't be tracked. Use uv add instead.
Q: Should I commit uv.lock?
Yes, always. The lock file is what makes installs reproducible. Without it, two people running uv sync on different days might get different package versions.
Q: What's the difference between uv add and uv sync?
uv addâ installs a new package and records it inpyproject.toml+uv.lockuv syncâ makes your environment match the currentuv.lock(used after pulling from git, or setting up a fresh clone)
Q: UV isn't found after installing (command not found)
Make sure ~/.local/bin is in your PATH:
export PATH="$HOME/.local/bin:$PATH"
Add that line to your ~/.bashrc or ~/.zshrc to make it permanent.
For the official UV docs: docs.astral.sh/uv