Anže's Blog

Python, Django, and the Web

29 Aug 2024

UV with Django

Astral made a huge summer splash in the Python community last week when they released uv 0.3.0.

What is uv?

uv is a Python package manager written in rust that has just gained the ability to be a project management tool (like Pipenv/PDM), tool management (pipx), python installer (pyenv), and more!

I was very eager to try it out on my Django projects, but the init rial 0.3.0 release was designed for managing installable Python packages, so there were a few rough edges when using it to manage Django app dependencies. Only a week later, these issues have been addressed, defaults were switched around, and uv 0.4.0 now supports Python projects like your Django application out of the box!

Using uv to create a new Django project

First, we’ll need to get uv. See the official docs for all the installation options, but the easiest way to get it on Linux and MacOS is to run:

curl -LsSf https://astral.sh/uv/install.sh | sh

Now that we have uv, we can use it to start a new project with:

❯ uv init hello-django
Initialized project `hello-django` at `/Users/anze/Coding/hello-Django`

Make sure you are using uv 0.4.0 or newer; older versions will presume you are creating an installable Python package, and you'll see "error: Failed to prepare distributions" errors when trying to run your project. You can upgrade your uv version by running the install command above.

You can now cd into the hello-django folder and see that uv created three files for us:

README.md
hello.py # we won't need this so feel free to rm it.
pyproject.toml

The pyproject.toml file is the most interesting one since it defines two important properties: requires-python and dependencies. The former defines which Python version we will be using, and the latter defines the project dependencies.

[project]
name = "hello-django"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []

Our Django projects won’t really care about the name, version, description, and readme properties, so just leave them as is.

Speaking of project dependencies. It’s about time we install Django with uv add django!

❯ uv add django
Using Python 3.12.5
Creating virtualenv at: .venv
Resolved 5 packages in 186ms
Prepared 3 packages in 3ms
Installed 3 packages in 235ms
 + asgiref==3.8.1
 + django==5.1
 + sqlparse==0.5.1

We can see that uv created a virtual environment (.venv folder), and installed Django with its two dependencies (asgiref and sqlparse). If we inspect the pyproject.toml file, we can see that Django was added to the dependencies list:

...
dependencies = [
    "django>=5.1",
]

The Django version has no upper bonds, so we can easily upgrade it when newer versions of Django come out (using uv lock --upgrade).

This doesn’t mean our current dependencies aren’t locked tight. Our whole dependency tree has its versions specified in the uv.lock file. The lock file is a cross-platform lock file, so it should be safe to install on any operating system!

Now that we have Django installed, we can run Django’s startproject command:

uv run django-admin startproject hello .

This initialized our Django project, including the manage.py file, hello/settings.py, etc.

We can start the Django development server with the following:

uv run manage.py runserver

Using uv with an existing Django project

If you already have a Django project, you can still use the uv init command to switch to uv.

cd to_existing_project
❯ uv init .
Initialized project `hello-django` at `/Users/anze/Coding/blog/hello-django`

If your project already has a pyproject.toml file defined the command might fail with:

error: Project is already initialized in `/Users/anze/Coding/blog/hello-django`

In that case, you’ll need to add a project table to your pyproject.toml file:

[project]
name = "hello-django"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []

And now, you should be able to add your existing dependencies to the pyproject.toml, either manually or with the uv add command. After all the dependencies are specified in pyproject.toml you can run uv sync to make sure everything is installed in your environment.

Installing dev dependencies

uv also supports installing development dependencies:

❯ uv add --dev pytest pytest-django
Resolved 11 packages in 8ms
Prepared 5 packages in 0.91ms
Installed 5 packages in 12ms
 + iniconfig==2.0.0
 + packaging==24.1
 + pluggy==1.5.0
 + pytest==8.3.2
 + pytest-django==4.8.0

You can now run your tests with

uv run pytest

The dev dependencies are saved in the tool.uv.dev-dependencies list in your pyproject.toml:

[tool.uv]
dev-dependencies = [
    "pytest>=8.3.2",
    "pytest-django>=4.8.0",
]

When deploying your Django application to production, you can avoid installing dev dependencies by running:

❯ uv sync --no-dev
Resolved 11 packages in 2ms
Uninstalled 5 packages in 35ms
 - iniconfig==2.0.0
 - packaging==24.1
 - pluggy==1.5.0
 - pytest==8.3.2
 - pytest-django==4.8.0

Avoiding uv run

Writing uv run gets very old very fast, but there are a few options to make the experience a bit nicer.

1. Aliases

You can alias uv run to something shorter like uvr:

alias uvr="uv run"

Or for Django use cases, you can define uvm like so:

alias uvm="uv run python manage.py"

So that you can run the manage.py file with only three letters:

uvm runserver

2. Adjusting the shebang line in manage.py

You can change the #!/usr/bin/env python at the top of manage.py into #!/usr/bin/env -S uv run to force invocations to use uv run.

./manage.py runserver

I learned about this trick from Jeff Triplett’s blog Python UV run with shebangs. 💚

Using uv run --with

uv run has another trick up its sleeve: an optional --with parameter that allows you to run your project with a different package version. This is super useful if you want to quickly verify if your project works with a newer (or older) version of Django:

❯ uv run python manage.py version
5.1
❯ uv run --with 'django<5' manage.py version
4.2.15
❯ uv run --with 'django<5' pytest # to run your tests on the latest 4.x version

Fin

This is a really exciting time for Python and Python packaging! If you want to learn more about uv check out the following links:

If you have any other tricks or ideas to simplify your Django workflows, let me know, and I’ll add your suggestions to the blog post!