Summary
You can test things in your venv without polluting it using uv run --with
, create almost standalone scripts with uv init --script
or convert a PDF to markdown by running uvx markitdown file.pdf
.
It's fast, it saves space, it avoids bootstrapping problems, and it makes Python workflows just a little nicer every day.
Soon my friend, soon...
While it is true I'm not ready to recommend uv
quite yet (I'm planning to write a full review in a few months to conclude my year of testing it), I have been using it extensively on my own projects, in training and with certain clients.
I also know a lot of you are as well, there is a real enthusiasm about the tool, so for those that made the jump, why not share cool tips about it?
Because one thing about uv
, is that it is independent of the Python runtime, yet capable of fully bootstrapping a whole project, and very quickly at that, so it enables novel use cases and patterns. Some are just new, some are things that were not practical before, and some are just old stuff but getting better.
uv run
uv run
will run any command inside the currently activated virtual environment, and if it's not activated but exists in the current directory with a common name (E.G: .venv
), then it will transparently use that.
But this command comes with some cool goodies.
First, it can run a command with a temporary dependency made available, without polluting your virtual environment. For example, let's say you want to create a jupyter
notebook and load your code in it. Normally, you'd have to install it in your project. But jupyter
is huge! It has many dependencies, and installing it would pollute your project.
With uv you can the --with
argument to provide a temporary dependency to your project and run a command:
uv run --with jupyter jupyter notebook
This will:
Create a temporary venv.
Install
jupyter
in it.Run the command with your project, your venv and the temporary venv available together transparently.
The result is that you can run jupyter
in your project without having to really install it in your project! Plus, the first time it's slow (it has to install everything), but the follow-up times, it will use a cache and it will be fast. Also, because of the way uv
deals with the cache (using hard links), if you have 10 projects that use the same version of jupyter
, you only install one copy of it instead of one for each project as before.
This works with everything. Want to run your project with ipython
? Wanna run ruff
or mypy
once only?
E.G: I like qtconsole
to do exploratory programming, but it installs jupyter
and qt5
, which means it's massive, so I rarely used it. Now it's just:
uv run --with pyqt5 --with qtconsole jupyter qtconsole
uv run
also understand PEP 723 inline dependencies. You have a quick script and you want to use typer for argument parsing? Before uv
, I used a shared venv for all my local scripts and installed the deps manually.
Now my script looks like this:
# /// script
# dependencies = [
# "typer",
# ]
# ///
import typer
def main(name: str):
print(f"Hello {name}")
if __name__ == "__main__":
typer.run(main)
This will automatically install the dependencies in a temp venv and run the script:
❯ uv run hello.py bitecode
Reading inline script metadata from: hello.py
Hello bitecode
On linux you can even add this shebang (then chmod +x
it), and ./hello.py
will automatically call uv run
:
#!/usr/bin/env -S uv run
If you don't remember how to create those scripts with inline deps, no worries, uv
has your back with uv init --script
:
❯ uv init --script my_script.py
Initialized script at `my_script.py`
❯ cat my_script.py
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
def main() -> None:
print("Hello from my_script.py!")
if __name__ == "__main__":
main()
uv init
is useful in general (it initializes a full project, including a .gitigore
and a minimal pyproject.toml
), but with —script
it only creates a single file, ready for scripting.
uvx
uvx
is a second command that is installed next to uv
when you set it up, and it's the equivalent of npx
or pipx
, but with uv
goodness. Meaning it's fast, it has great caching, supports git repositories, is in full rust (so no chicken and egg or PATH problem), and has the --with
option.
This is great for two things:
quickly testing stuff.
running Python utilities without worrying about setup.
You want to test pendulum
in an ipython
shell with python 3.13?
❯ uvx --with pendulum -p 3.13 ipython
Installed 21 packages in 27ms
Python 3.11.11 (main, Dec 4 2024, 08:55:08) [GCC 13.2.0]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.30.0 -- An enhanced Interactive Python. Type '?' for help.
Exception reporting mode: Plain
Doctest mode is: ON
>>> import pendulum
>>> pendulum.now()
DateTime(2024, 12, 15, 21, 52, 56, 856087, tzinfo=Timezone('Europe/Paris')
You want to run the latest textual demo from the Will McGugan?
Note how my first attempt failed, but the excellent error message made it instantly solvable.
Want to download the mp3 of our Charlie Marsh (uv's team leader) interview (this does require ffmpeg to be installed prior)?
uvx --no-cache --from yt-dlp[default] yt-dlp --extract-audio --audio-format mp3 https://www.bitecode.dev/p/charlie-marsh-on-astral-uv-and-the
This works on YouTube or Soundcloud, thanks to the excellent yt-dlp project. --no-cache
forces yt-dlp to update every time, which is required given the countermeasures change so often and the setup time is tiny compared to the video download time.
Python has many such tools you want to fire once in a while and forget it:
cog: the weird and excellent inline templating tool.
httpie: the better alternative to
curl
for simple HTTP requests.copier: the success of cookiecutter to initialize projects.
markitdown: the office/pdf to markdown converter.
All those little things that used to be frictions are now an uv
away, making our Python workflows a bit nicer every day.
The cache can grow quite a lot and quickly though, because it's so tempting to use it all the time. Cleaning up may be a good idea once in a while:
$ uv cache clean
Clearing cache at: /home/user/.cache/uv
Removed 269615 files (23.8GiB)
Thank you for the article. Regarding scripts, I find the uv add —script method very convenient instead of the init script method. (https://docs.astral.sh/uv/guides/scripts/#declaring-script-dependencies)
Still not into uv; it feels like giving up on python to be writing its packaging tools in a rival language. At least conda is almost entirely in python.