From managing dependencies to running scripts, uv is changing the way we handle Python projects. This guide breaks down how to use uv for lightning-fast, reliable Python development.
Python packaging has come a long way since the dark days of easy_install
, but let’s be honest — it’s still a bit of a mess. You've got pip
for installing, venv
for isolation, requirements.txt
for pinning, pip-tools
for syncing, poetry
if you're feeling modern, and a whole lot of “wait, why is this broken now?” in between. Every tool solves part of the puzzle, and yet somehow the whole still feels like a Jenga tower waiting for a sneeze.
Enter uv
: a blazing-fast Python package and project manager written in Rust by the folks at Astral (yes, the same people who built the Ruff linter). Think of uv
as the multi-tool that’s out to replace your cluttered toolbox.
It installs dependencies. It locks them. It syncs them. It runs scripts. It caches aggressively.
In this post, we're going to break down everything uv
can do — from managing dependencies and lockfiles, to caching, script running, Python version pinning, and its nifty sibling uvx
. Whether you're a solo dev trying to wrangle your personal projects, or a team lead desperate for reproducible builds that don’t feel like a gamble, uv
might just be your new favorite tool.
Let’s dive in — no dependency hell required.
By the way, here's the video version of this article. Perfect if you want a short, high level overview before diving into this more in depth article.
Before you unleash the speed and ease of uv
, you’ll need to install it. Fortunately, the team at Astral understands that nobody wants to fight their package manager just to install a new package manager.
Here’s the quickest way to get started:
MacOS and Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
Windows
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
That command downloads a prebuilt binary for your platform which is ready to go. No virtual environment setup. No pip
spaghetti.
Prefer something more package-manager-official? You’ve got options:
Homebrew (macOS/Linux):
brew install astral-sh/uv/uv
pipx:
pipx install uv
Once installed, verify it’s working:
uv --version
You should see something like uv x.y.z
depending on the version.
If you’ve used pip
, you know the dance: install a package, maybe pin a version, maybe forget to update your requirements.txt
, maybe break your environment six months later. uv
simplifies this by combining dependency management and locking into one fast, streamlined experience.
Here’s how it works.
Need a package? Just ask:
uv add requests
This does three things in one shot:
pyproject.toml
with the new dependencyuv.lock
file to lock it all downNo manual edits, no extra sync steps, no “did I forget to regenerate my lockfile?” surprises.
Want to specify a version?
uv add requests==2.31.0
It’s as explicit as you want it to be.
Upgrading dependencies with uv
is done with the --upgrade
parameter:
uv add requests --upgrade
This safely upgrades the package and rewrites the lockfile to reflect the new version.
Uninstalling dependencies is just as easy as adding them:
uv remove requests
No orphaned packages. No lockfile drift.
Want to add tools like linters, formatters, or test runners without polluting your production environment?
uv add --dev black pytest
uv
tucks them away under [tool.uv] dev-dependencies
in your pyproject.toml
, keeping your runtime environment clean and your tooling close at hand.
uv run
makes sure your scripts run in the context of your project’s environment — using exactly the versions you’ve declared and locked.
Think of it like npm run
or poetry run
, but faster and with less ceremony.
Say you’ve added black
and pytest
as dev dependencies. You can run them like this:
uv run black .
uv run pytest
No need to activate a virtual environment. No need to remember which version of black you installed. uv
reads from your lockfile and runs the exact version you’ve specified.
uv run
executes commands in your project's environment — using only the dependencies declared in your lockfile.
Want to run my_script.py
?
uv run python my_script.py
Working on a DJango project? Start the dev server like so:
uv run manage.py runserver
It's exactly like using a virtual environment — minus the manual setup, activation, deactivation, and that one time you accidentally ran pip install
globally (we’ve all been there).
Now that you're installing and running packages with ease, let’s talk about how uv
keeps your project organized and reproducible. This is where the real magic happens — not with vague “it probably works” environments, but with deterministic, lockfile-powered precision.
pyproject.toml
is the central configuration file for modern Python projects — and uv
uses it as the source of truth for your dependencies.
When you run something like:
uv add requests
uv
adds that dependency to the pyproject.toml
under the appropriate section (either [project.dependencies]
or [tool.uv.dev-dependencies]
, depending on whether you use --dev
). It doesn't invent a new standard — it builds on the one the Python ecosystem is already rallying around.
The result: your project metadata and dependencies are all in one place. Clean. Editable. Version-controlled. As it should be.
Where pyproject.toml
defines what you want, uv.lock
defines exactly what you're getting — down to the last transitive dependency.
This lockfile is automatically created or updated whenever you run commands like uv add
, uv remove
, or uv sync
. It ensures that every machine (or CI pipeline) installs the same versions, every time.
No more "but it worked yesterday." No more "why does the same install produce different results?" Just a lockfile you can trust.
Need to update everything? Run:
uv sync --upgrade
Want to just make sure your environment matches the lockfile?
uv sync
Curious about what's actually installed? uv gives you visibility without the clutter:
uv tree
This prints a neatly formatted dependency tree based on your lockfile. No more digging through pip freeze
outputs wondering who brought in urllib3
five times. You’ll see exactly what’s being pulled in, and why.
You can even use --all
to include dev dependencies or --top-level-only
for a more minimal view.
With pyproject.toml
giving structure, uv.lock
providing reproducibility, and uv tree
offering transparency, uv helps you treat your project like a first-class citizen — not a pile of versioned guesses.
Most Python developers have a common ritual: install dependencies, wait a bit, install them again somewhere else, wait again, and then—just for fun—wait a third time in CI.
uv
breaks the loop.
One of uv’s standout features is its aggressive caching strategy. When you install a package, uv
:
The next time you install the same thing? It's instant.
Where does it stash these caches? By default, in a system-wide location (~/.cache/uv
on Unix-like systems). This means:
No more redundant downloads. No more rebuilding wheels. uv
respects your time — and your bandwidth.
Worried about your cache becoming a landfill of old versions? uv
handles it gracefully.
uv cache clean
Dependencies are only half the story. The other half? The actual Python interpreter running your code.
With uv
, you're able to manage globally available Python versions as well as the specific version you need in your project.
To see which versions of Python are available as well as which of those are installed on your machine, you can run:
uv python list
Installing a new version of Python globally on your machine is as simple as:
uv python install 3.12
Simply specify which version you want. Quick and easy.
By creating a .python-version
file in your project, you can specify which version of Python to use. This version will be specific to your project and can be different from your global machine version.
This is a simple text file that contains the version of Python your project expects:
3.13
When you run uv
, it checks this file to figure out which Python version to use within your project.
Here’s where it gets interesting: uv
doesn’t just use .python-version
as a suggestion — it actually resolves dependencies based on the specified Python version.
That means:
uv
are Python-version-specific, preventing unpleasant surprises across machines or CI environments.Want to change the Python version for your project? Just update .python-version
, and re-sync:
uv sync
uv
will re-resolve everything to match the new interpreter version.
No more crossing your fingers after switching to a new Python version. uv
ensures that your lockfile reflects the actual supported dependencies for that specific interpreter version. And that includes platform-specific wheels too.
Whether you’re building on macOS, deploying to Linux, or testing on Windows, uv
keeps things sane and consistent.
In short: .python-version
plus uv
means your environment isn’t just “close enough.” It’s exact, predictable, and rock-solid — even across teams and systems.
Sometimes, you just want to run a tool and move on with your life. You don’t want to add it to your project. You don’t want to pin it. You definitely don’t want to argue with pip
.
Enter uvx
.
uvx
is your go-to for running command-line tools without installing them into your project. Think of it like npx
for Python: ephemeral, efficient, and always pulling the latest version (unless told otherwise).
Want to run black
just once, without polluting your environment?
uvx black .
That’s it. No project changes, no lockfile updates. Just a clean run.
Use uvx
when:
It’s perfect for utilities like:
Or even your own CLIs published to PyPI.
Behind the scenes, uvx
downloads and caches the tool (fast, of course), then runs it in a temporary environment.
No installs. No cleanup. No regrets.
Important note: tools run with uvx
aren’t added to your lockfile, aren’t managed by your project, and won’t be available in uv
run.
So if you want something to be part of your reproducible, locked setup? Use uv add
instead.
With uvx
, the tooling barrier drops to near zero. Try, test, explore — without clutter.
But when you’re ready to commit, bring those tools into your workflow properly using the dependency commands we covered earlier.
Python packaging has been... a journey. We've lived through pip
, pipenv
, poetry
, virtual environments, lockfiles, dependency hell, and mysterious import errors that only happen on Wednesdays.
Enter uv
, where you get:
uv run
uvx
And it’s all wrapped in a single tool that Just Works™ — whether you’re a solo developer or managing a production pipeline.