Configuration of Python Projects and Tools with UV#

We will create a Python project using uv, which utilizes the pyproject.toml format for its configuration, making it easier to port across different environments. This format is also already widely used by other package managers of Python, and is commonly used for configuring utilities.

For illustration purposes, I’ll be migrating the blog management to UV, of which I had already mentioned its benefits as an alternative to PIP and VENV.

Creating a Project with uv init#

If we create one from scratch, the project name will be the directory name, but if we’re inside and use the existing directory, it will be assigned as the project name. In my case, the existing directory is my GitHub repository for cosmoscalibur.github.io, and this will be the project name.

We don’t always need to use the same version of Python, and on Linux, we have it installed by default, but not everyone has the same version. Therefore, it’s convenient to specify the version in the pyproject.toml configuration file, which generates files such as .python-version, an example hello.py file.

uv init . -p 3.12

When we’re inside the directory, we can run uv run hello.py, which will use the specified Python version and build the necessary environment to work. The environment will be created in the .venv subdirectory. Since we’ve done this test, you can delete the example file now.

To add dependencies to the project, we use uv add. We can add them individually or a list of packages.

uv add sphinx
uv add ablog
uv add pydata-sphinx-theme
uv add sphinx-design sphinx-copybutton sphinxcontrib-youtube sphinxext-rediraffe
uv add sphinxext-opengraph matplotlib
uv add myst-parser jupytext myst-nb

These additions are appended to the project’s dependencies field in the pyproject.toml file.

By default, if no explicit restriction is specified, it will use the maximum compatible version available in the environment. If you need an explicit restriction, you can specify it using the same syntax as with pip, and it must be enclosed within quotes.

Optional dependencies also have their own addition mechanism, which I’ll take advantage of to add one that I’m replacing with my own management approach, but would like to compare. This is done by adding --optional <group_name> followed by the name of a group for this optional dependency.

uv add sphinx-sitemap --optional sitemap

We can also explicitly specify which dependencies are for development by using uv add --dev. Or, if we want to indicate a specific group of development dependencies, we use uv add --group <group_name> followed by the name of the group and then the packages.

uv add --dev jupyterlab jupyterlab_myst
uv add --group rest rstcheck doc8 docstrfmt "esbonio>=1.0.0b8"
uv add --group markdown mdformat mdformat-gfm mdformat-frontmatter \
  mdformat-footnote mdformat-gfm-alerts mdformat-myst

It’s worth noting that the --dev option is a shortcut for specifying the “dev” group (--group dev), which is the default group.

Running Entry Points and Scripts with uv run#

As with any virtual environment, this one can be activated and once it’s done, you can run entry points (the executable utility or function) or routines by invoking python.

source .venv/bin/activate # With source
pipenv shell # With pipenv (also on Windows)

If you don’t have pipenv, you can install it as a tool using the command uv tool install pipenv.

However, uv itself offers a convenient way to run without explicitly activating the environment, which is very useful in many cases, such as when running a justfile (which we’ll cover later). From the initial example, you already know how to run a routine, but if you need an executable utility from the environment, you have a similar approach by initializing with uv run --.

uv run hello.py  # Ejecutar rutinas Python
uv run -- ablog clean  # Ejecutar utilidades en el ambiente

If the environment hasn’t been created, this step creates it in .venv or ensures that the environment is up-to-date. Additionally, it also creates uv.lock, a format that ensures the exact reproducibility of environments. This file can be created using uv sync and uv lock.

Installing Tools with uv tool#

In the previous example, packages added with groups do not require explicit dependency on the project and can be installed globally. This type of usage corresponds to the concept of tools, and with uv we can manage it in two ways:

  • Temporal: They are stored in cache and always use the uvx prefix. If they’re not installed, they get installed and executed; if they’re already installed, they run as usual.

  • Permanent: They are linked to visibility across the entire OS with direct invocation of application’s entry point. In this case, installation is required as a first step, and then invoked traditionally. I prefer this case because I often use them in other projects, but for cloud or isolated projects, the previous option is suitable. This is done using uv tool install.

For ReST-related packages in my repo, each one is a separate tool, so we proceed with installing each one individually.

uv tool install rstcheck
uv tool install doc8
uv tool install docstrfmt
uv tool install esbonio --prerelease=allow

Just like when adding dependencies, if no explicit version is specified, the most recent one is used. In the case of esbonio, there’s a parameter --prerelease=allow that allows us to use pre-release versions (alpha, beta, candidates), but we can also specify an explicit version.

For Markdown packages in the group, these are actually extensions of a main package, mdformat. To inject additional dependencies, we do it using the parameter --with.

uv tool install mdformat \
  --with mdformat-gfm \
  --with mdformat-frontmatter \
  --with mdformat-footnote \
  --with mdformat-gfm-alerts \
  --with mdformat-myst

Finally, although the options are illustrated, as I’m testing modifications in extensions, I also need Ruff for code adjustments (although this may not be necessary depending on the IDE used)

uv tool install ruff

References#