I was surprised at how painless it was to switch things over. You can see the git diffs to make the change for both of my example Flask and Django projects. In this post we’ll go into more detail about these changes and how to use a few uv commands.
Let’s start with defining our project’s dependencies.
You can create a pyproject.toml file and delete your requirements.txt after you’ve entered your project’s dependencies and their versions into pyproject.toml.
You only need to add your top level dependencies, uv will make a lock file for you automatically which is somewhat comparable to what pip freeze would produce except uv’s lock file has proper dependency trees and is way better.
Here’s a very small diff that shows an example of what to do, adjust it as needed:
In both cases I extracted their install commands to a separate script so it’s easy to either run at build time in the Dockerfile (as seen above), or by running it as a command at run-time to make sure your lock file gets updated on your host machine through a volume.
In any case, both solutions are just shell scripts. Here’s the one for uv with comments:
#!/usr/bin/env bash
set -o errexit
set -o pipefail
# Ensure we always have an up to date lock file.
if ! test -f uv.lock || ! uv lock --check 2>/dev/null; then
uv lock
fi
# Use the existing lock file exactly how it is defined.
uv sync --frozen --no-install-project
There’s a few ways to use uv, such as using its pip sub-command but I like using sync since it’s the “uv way” of doing things. The pip sub-command is there to help create a mental model of how uv works, or continue using pip’s commands through uv if you prefer.
The --frozen flag ensures the lock file doesn’t get updated. That’s exactly what we want because we expect the lock file to have a complete list of exact versions we want to use for all dependencies that get installed.
The --no-install-project flag skips installing your code as a Python package. Since we have a pyproject.toml with a project defined the default behavior is to install it as a package.
For a typical web app, you usually have your project’s dependencies and that’s it. Your project isn’t an installable project in itself. However, if you do have that use case feel free to remove this flag! You can think of this as using --editable . with pip.
If you’re using my example starter app, it comes with a few run script shortcuts. They’re shortcut shell scripts to run certain commands in a container:
./run deps:install
Build a new image and volume mount out a new lock file
It’s mainly doing docker compose build and running bin/uv-install inside of a container which has a volume mount so your host’s lock file gets updated
./run deps:install --no-build
The same as above except it skips building but still mounts out a new lock file
1:36 – pyproject.toml to replace requirements.txt
3:05 – Dockerfile: install uv
3:56 – Dockerfile: dependency files
4:50 – Dockerfile: env vars
6:46 – Dockerfile: uv lock / sync
10:22 – Quick recap
10:44 – One way to update a package
11:41 – Checking for outdated packages
13:29 – Using uv add to add or update packages
15:27 – Adding a new package at its latest version
16:12 – Removing a package
Did you switch to uv, how did it go? Let me know below.
Like you, I'm super protective of my inbox, so don't worry about getting spammed. You can expect a few emails per year (at most), and you can 1-click unsubscribe at any time. See what else you'll get too.