<a id="how-manage-python-environments"></a>

# How to manage Python environments with the uv SDK

<!-- @tests in tests/docs-how-to/manage-python-environments/task.yaml -->
<!-- @artefact uv SDK -->

The `uv` SDK is the recommended way to manage Python projects in **Workshop**.
It ships **uv** and **uvx**,
aliases **pip** to **uv pip** for compatibility,
and exposes a virtual environment slot
that other Python-based SDKs (such as `jupyter`) can plug into.
The typical day-to-day flow boils down to running **uv** commands
against a project virtual environment whose location you control.

## Add the uv SDK to your workshop

Add `- name: uv` to the workshop definition;
the SDK tracks the `latest/stable` channel by default,
which is sufficient for most projects:

```yaml
name: pyenv
base: ubuntu@24.04
sdks:
  - name: uv
```

Place your project at the host directory
that you launch the workshop from;
**Workshop** bind-mounts it inside the workshop at `/project/`.
The project should contain a `pyproject.toml`
(or a `requirements.txt`)
that **uv** can read.

## Run uv from your project

Open a shell in the workshop and use **uv** from `/project/`:

```console
$ workshop shell
workshop@pyenv:~$ cd /project
workshop@pyenv:/project$ uv sync
workshop@pyenv:/project$ uv add requests
workshop@pyenv:/project$ uv run python -c "import requests"
```

**uv sync** resolves the project’s dependencies
and creates a virtual environment at `/project/.venv`,
that is, next to your `pyproject.toml`.
**uv run** and **uv add** then operate against that environment.
The venv lives on the host filesystem
because `/project/` is the bind mount,
so it survives container refreshes
and is visible to host-side tooling such as IDEs.

The `uv` SDK aliases **pip** to **uv pip**
through **update-alternatives**;
running **pip install <PACKAGE>** from a workshop shell
transparently invokes **uv pip install <PACKAGE>**.

## Inspect what the SDK configures

The SDK applies a few defaults
so that **uv** works correctly with the workshop’s storage layout:

<!-- @artefact UV_LINK_MODE -->
- `UV_LINK_MODE` is set to `copy` in the workshop user’s environment
  because the persistent cache mount does not support hardlinks.
  You don’t need to set this yourself.
- The **uv** package cache is persisted on the host
  through a `mount` interface plug
  that maps `/home/workshop/.cache/uv/` to durable storage,
  so cached downloads survive workshop updates.
- A shared virtual environment slot is exposed
  at `/home/workshop/uv-venv/`.
  This slot exists for cross-SDK sharing
  (see the next section);
  it is *not* the venv your project uses by default.

<a id="how-manage-python-environments-share"></a>

## Share the environment with another SDK

To run a Python SDK such as JupyterLab against a uv-managed environment,
add the `jupyter` SDK alongside `uv`
and connect `jupyter:venv` to `uv:venv` explicitly
in the workshop definition:

```yaml
name: pyenv
base: ubuntu@24.04
sdks:
  - name: uv
  - name: jupyter
connections:
  - plug: jupyter:venv
    slot: uv:venv
```

An explicit `connections:` block is required:
without it, `jupyter:venv` falls back to `system:mount`
(the host directory **Workshop** provides as a default plug target)
and the two SDKs don’t share an environment.

After **workshop refresh**,
**workshop connections --all** confirms the wiring:

```console
$ workshop connections --all

  INTERFACE  PLUG                SLOT                NOTES
  mount      pyenv/jupyter:venv  pyenv/uv:venv       -
  mount      pyenv/uv:cache      pyenv/system:mount  -
```

Packages that `jupyter` installs into its venv
now land in the shared environment provided by `uv`,
so **jupyter** and **uv run** see the same dependency set.

<a id="how-manage-python-environments-pin"></a>

## Pin the project venv with UV_PROJECT_ENVIRONMENT

By default, **uv** places the project virtual environment
at `.venv` next to the `pyproject.toml` it discovers,
which inside a workshop is normally `/project/.venv`.

Override this default with `UV_PROJECT_ENVIRONMENT`
when you want a single, explicit venv location
regardless of where in the project tree **uv** is invoked,
or when several workshops share the same project directory
and should reuse one venv.

For example, to place the venv at `/project/pinned-venv`
instead of the default `/project/.venv`:

```console
$ echo 'export UV_PROJECT_ENVIRONMENT=/project/pinned-venv' >> ~/.profile
$ exec bash -l
$ cd /project
$ uv sync
```

Relative paths are resolved from the workspace root,
absolute paths are used as is;
if the environment does not exist at the specified path,
**uv** creates it.

#### WARNING
Set `UV_PROJECT_ENVIRONMENT` to a path *inside* `/project/`,
such as `/project/.venv/`,
not to `/project/` itself.
**uv** writes the venv layout
(`bin/`, `lib/`, `pyvenv.cfg`)
directly under the value you provide,
so a bare `/project/`
would scatter venv files across your project sources.

## See also

Explanation:

- [SDK dependencies](https://ubuntu.com/workshop/docs//explanation/sdks/best-practices.md#exp-best-dependencies)
- [Mount interface](https://ubuntu.com/workshop/docs//explanation/interfaces/mount-interface.md#exp-mount-interface)
- [Workshop definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition)

How-to guides:

- [How to run JupyterLab in your browser](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/run-jupyterlab-in-browser.md#how-jupyterlab-run-in-browser)
