1354 words
7 minutes
English

Making mise and uv Agree on Python

I noticed the problem because the same command returned different versions in two directories:

Terminal window
$ cd ~/project && uv run python --version
Python 3.12.7
$ cd ~ && uv run python --version
Python 3.13.1

The project result was expected: each project can pin its own runtime. The result in my home directory was not. I did not remember installing Python 3.13.1, and mise—which I use as the sole owner of runtime versions, as described in the first article in this series—did not know about it.

Finding the other Python#

I first checked what python referred to in each context. The shell and uv run were using different binaries:

Terminal window
$ which python
/Users/me/.local/share/mise/installs/python/3.12.7/bin/python
$ uv run python -c 'import sys; print(sys.executable)'
/Users/me/.local/share/uv/python/cpython-3.13.1-macos-aarch64-none/bin/python3.13

The shell found the interpreter installed by mise and exposed on PATH. Outside a pinned project, however, uv run selected an interpreter under ~/.local/share/uv/python, a directory I had never intentionally populated.

uv’s own list made the split explicit:

Terminal window
$ uv python list --only-installed
cpython-3.13.1-macos-aarch64-none /Users/me/.local/share/uv/python/cpython-3.13.1-.../bin/python3.13
cpython-3.12.7-macos-aarch64-none /Users/me/.local/share/mise/installs/python/3.12.7/bin/python3.12

There were two separate CPython builds. readlink -f confirmed that these were not two paths to the same binary: one belonged to mise, while the other had been downloaded and stored by uv.

This is normal uv behavior, not a bug. Its python-preference setting defaults to managed: prefer a uv-managed Python and, when no available interpreter satisfies a project’s requires-python, download one. That is convenient when uv owns both the runtime and the packages. It conflicts with my setup because mise is supposed to be the only tool deciding which Python versions exist.

Interpreter, virtual environment, and cache#

Before changing the setting, I needed to separate three things that all look like “Python files in hidden directories” but have different lifetimes and owners.

Blueprint, sample unit, and shared library: mise install, .venv, and the uv cache Blueprint mise install python 3.14.0 one full interpreter per version number many versions coexist Sample unit project/.venv bin/python → symlink to the blueprint site-packages = the only private part Shared library uv cache content-addressable one copy of a package shared across all projects symlink link in

I think of a version installed by mise install python@3.14 as a blueprint: one complete interpreter identified by a version number. Several versions can coexist in mise’s install directory, so 3.12.7 and 3.14.0 are simply two independent blueprints.

A project’s .venv is a sample unit made from one of them. A virtual environment is mostly symlinks; its bin/python points back to the source interpreter instead of containing another copy. Its genuinely project-private part is site-packages. This makes a .venv small and quick to create, but it also means that removing a mise-managed Python breaks every environment whose bin/python points to it.

The uv cache is the shared library. It is content-addressable: a package version is stored once, then linked into each project’s site-packages rather than repeatedly copied. If ten projects use the same version of numpy, they can refer to the same cached copy. Much of uv’s speed comes from creating these links instead of downloading and unpacking the same files again.

In this model, mise supplies interpreters. uv creates project environments from them and fills those environments from its package cache. I did not need uv to provision another interpreter as well.

Restricting uv to system Python#

uv reads global configuration from ~/.config/uv/uv.toml. I added one setting:

~/.config/uv/uv.toml
python-preference = "only-system"

The four possible values matter:

  • only-managed uses only uv-managed Pythons and ignores system interpreters. It provides the strongest isolation from the system and is the furthest from a mise-owned setup.
  • managed, the default, prefers uv-managed Pythons, falls back to system Pythons, and downloads a managed interpreter if neither satisfies requires-python. This was the source of my unexpected installation.
  • system prefers an interpreter already on PATH, but can still download a managed one when no suitable system interpreter exists.
  • only-system uses only system Pythons, including those that mise places on PATH, and never downloads one. If none satisfies the requirement, the command fails.

For my setup, that final failure mode is useful: a missing version remains visible until I install it through mise. Someone who wants to prefer mise without giving up uv’s fallback can choose system instead.

After changing the setting, I removed uv’s copy of Python 3.13.1 and rebuilt the affected environment:

Terminal window
$ uv python uninstall 3.13.1
$ cd ~/project && rm -rf .venv && uv sync

uv run python and the shell’s python then agreed because both resolved to an interpreter owned by mise.

Existing projects can fail after the change#

Changing a global selection rule also affects environments created under the old rule. I had older projects whose mise.toml requested Python versions that I had since removed from mise. Previously, uv sync could download the missing interpreter and continue. With only-system, it stopped:

error: No interpreter found for Python 3.11 in system path

That error means the project requests a Python that is not currently installed. The explicit repair is:

Terminal window
$ mise install python@3.11 # provision the blueprint, deliberately
$ uv sync # now succeeds, using mise's Python

When reopening an older project, I now:

  1. Check requires-python in pyproject.toml and any version pinned in mise.toml.
  2. Run mise install if that interpreter is missing.
  3. Run rm -rf .venv && uv sync so the environment points to the installed interpreter.
  4. Confirm the result with uv run python --version.

This adds a deliberate mise install step where the default configuration might have fixed the mismatch silently. I prefer seeing the missing runtime, but it is a real trade-off rather than free strictness.

I also keep uv.toml in my chezmoi source repository, alongside the files discussed in the dotfiles article, so the same rule follows me to another machine.

Small scripts do not need full projects#

For a 40-line script that needs httpx, creating pyproject.toml, a .venv, and a lockfile is unnecessary. uv offers smaller paths that still obey only-system.

For a throwaway run, --with adds dependencies to a temporary environment:

Terminal window
$ uv run --with httpx --with rich script.py

For a script worth keeping, PEP 723 metadata records the Python requirement and dependencies in the file:

# /// script
# requires-python = ">=3.12"
# dependencies = ["httpx", "rich"]
# ///
import httpx
from rich import print
print(httpx.get("https://example.com").status_code)

Running uv run script.py makes uv read that metadata, assemble an environment from its cache, and execute the script without a project .venv. With a uv shebang, chmod +x, and a location such as ~/.local/bin, the script can also act like any other command on PATH while keeping its dependencies in the file. This has been much cleaner than the personal utilities I used to install into a global interpreter with pip install and later forget about.

Actual command-line tools belong in isolated tool environments. uv tool install handles persistent installations, while uvx runs a tool once; both fit utilities such as ruff or httpie better than a global pip install.

If mise is absent, letting uv own both Python and its packages is internally consistent, and the default managed behavior may be exactly right. My conflict existed only because two tools were making runtime decisions. The next article looks at how I decide when the official tool should own that layer instead.

Making mise and uv Agree on Python
https://www.shiinayane.com/en/posts/python/
Author
YANKAI WANG
Published at
2026-05-30
License
CC BY-NC-SA 4.0