Skip to content

Conversation

@jtfmumm
Copy link
Contributor

@jtfmumm jtfmumm commented Jun 10, 2025

NOTE: The PRs that were merged into this feature branch have all been independently reviewed. But it's also useful to see all of the changes in their final form. I've added comments to significant changes throughout the PR to aid discussion.

This PR introduces transparent Python version upgrades to uv, allowing for a smoother experience when upgrading to new patch versions. Previously, upgrading Python patch versions required manual updates to each virtual environment. Now, virtual environments can transparently upgrade to newer patch versions.

Due to significant changes in how uv installs and executes managed Python executables, this functionality is initially available behind a --preview flag. Once an installation has been made upgradeable through --preview, subsequent operations (like uv venv -p 3.10 or patch upgrades) will work without requiring the flag again. This is accomplished by checking for the existence of a minor version symlink directory (or junction on Windows).

Features

  • New uv python upgrade command to upgrade installed Python versions to the latest available patch release:
# Upgrade specific minor version 
uv python upgrade 3.12 --preview
# Upgrade all installed minor versions
uv python upgrade --preview
  • Transparent upgrades also occur when installing newer patch versions:
uv python install 3.10.8 --preview
# Automatically upgrades existing 3.10 environments
uv python install 3.10.18
  • Support for transparently upgradeable Python bin installations via --preview flag
uv python install 3.13 --preview
# Automatically upgrades the `bin` installation if there is a newer patch version available
uv python upgrade 3.13 --preview
  • Virtual environments can still be tied to a patch version if desired (ignoring patch upgrades):
uv venv -p 3.10.8

Implementation

Transparent upgrades are implemented using:

  • Minor version symlink directories (Unix) or junctions (Windows)
  • On Windows, trampolines simulate paths with junctions
  • Symlink directory naming follows Python build standalone format: e.g., cpython-3.10-macos-aarch64-none
  • Upgrades are scoped to the minor version key (as represented in the naming format: implementation-minor version+variant-os-arch-libc)
  • If the context does not provide a patch version request and the interpreter is from a managed CPython installation, the Interpreter used by uv python run will use the full symlink directory executable path when available, enabling transparently upgradeable environments created with the venv module (uv run python -m venv)

New types:

  • PythonMinorVersionLink: in a sense, the core type for this PR, this is a representation of a minor version symlink directory (or junction on Windows) that points to the highest installed managed CPython patch version for a minor version key.
  • PythonInstallationMinorVersionKey: provides a view into a PythonInstallationKey that excludes the patch and prerelease. This is used for grouping installations by minor version key (e.g., to find the highest available patch installation for that minor version key) and for minor version directory naming.

Compatibility

  • Supports virtual environments created with:
    • uv venv
    • uv run python -m venv (using managed Python that was installed or upgraded with --preview)
    • Virtual environments created within these environments
  • Existing virtual environments from before these changes continue to work but aren't transparently upgradeable without being recreated
  • Supports both standard Python (python3.10) and freethreaded Python (python3.10t)
  • Support for transparently upgrades is currently only available for managed CPython installations

Closes #7287
Closes #7325
Closes #7892
Closes #9031
Closes #12977

PythonSource::Managed,
if version.patch().is_some() || is_alternative_implementation {
installation.executable(false)
} else {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When deriving an executable from an installation, if the version is not a patch and it's a CPython installation, uv looks for a minor version symlink directory. If one is found, it will use the symlink-directory containing path. This enables transparently upgradeable virtual environments created with the venv module.

.patch()
.is_some_and(|p| p >= highest_patch)
{
installed.ensure_minor_version_link(preview)?;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uv checks for the highest patch for each minor version key and ensures that the corresponding minor version link exists and points to it (but a symlink directory is only initially created in preview mode).

/// A view into a [`PythonInstallationKey`] that excludes the patch and prerelease versions.
#[derive(Clone, Eq, Ord, PartialOrd, RefCast)]
#[repr(transparent)]
pub struct PythonInstallationMinorVersionKey(PythonInstallationKey);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used for grouping by the full key minus the patch and prerelease. Also used for deriving symlink directory names.


/// Calls `fs_err::canonicalize` on Unix. On Windows, avoids attempting to resolve symlinks
/// but will resolve junctions if they are part of a trampoline target.
pub fn canonicalize_executable(path: impl AsRef<Path>) -> std::io::Result<PathBuf> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been moved from uv_fs because it now knows about trampolines. If the path is to a trampoline, this will canonicalize the trampoline's internal Python path. If not, it will behave as before.

// Only execute the trampoline again if it's a script, otherwise, just invoke Python.
match kind {
TrampolineKind::Python => {}
TrampolineKind::Python => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updates to the trampoline logic to set the __PYVENV_LAUNCHER__ env var for a Python launcher (the approach taken by CPython for Python Launchers) and, if it's not a virtual environment and the PYTHONHOME is not set, to set the PYTHONHOME env var to ensure the correct directories are added to sys.path when running with a junction trampoline.

let path = dunce::canonicalize(path.as_path()).unwrap_or_else(|_| {
error_and_exit("Failed to canonicalize script path");
});
let path = if !path.is_absolute() || matches!(kind, TrampolineKind::Script) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This update is required so we don't resolve junctions for junction trampolines. We need to be able to use the junction to ensure transparent upgrades.

// Create a `.gitignore` file to ignore all files in the venv.
fs::write(location.join(".gitignore"), "*")?;

let executable_target = if upgradeable && interpreter.is_standalone() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is an upgradeable request, we'll try to use a Python minor version link when creating the virtual environment (creating the symlink directory/junction if it doesn't exist).

.into_iter()
.map(|a| InstallRequest::new(a, python_downloads_json_url.as_deref()))
.collect::<Result<Vec<_>>>()?
if upgrade {
Copy link
Contributor Author

@jtfmumm jtfmumm Jun 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is an upgrade with empty targets, uv collects minor version requests.

existing_installations,
);

for installation in minor_versions.values() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure the minor version links exist for the highest patch installations per minor version key.

for target in targets {
let target = bin.join(target);
match installation.create_bin_link(&target) {
let executable = if upgradeable {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When creating bin links, we now try to use a symlink directory as the bin link target if we can and this is an upgradeable request.

// For each uninstalled installation, check if there are no remaining installations
// for its minor version. If there are none remaining, remove the symlink directory
// (or junction on Windows) if it exists.
for installation in &matching_installations {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's where we uninstall symlink directories if there are no installations remaining for the corresponding minor version key.

let reinstall = false;
let upgrade = true;

commands::python_install(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had initially tried creating a new python_upgrade command, but there is so much shared machinery that it seemed like an unnecessary amount of maintenance overhead to split the code paths. Sharing them also makes adding python install --upgrade trivial if we decide to go that route.

@jtfmumm jtfmumm marked this pull request as ready for review June 10, 2025 20:27
@jtfmumm jtfmumm requested review from konstin and zanieb June 10, 2025 21:00
@jtfmumm jtfmumm force-pushed the feature/transparent-python-upgrades branch from 942f47c to 56b5fda Compare June 11, 2025 13:22
Comment on lines +639 to +644
let path = path.as_ref();
debug_assert!(
path.is_absolute(),
"path must be absolute: {}",
path.display()
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a part of this pull request, but we should follow up on it.

I'm still confused by this, why must the path be absolute? That isn't documented for this function. I think I asked this in a previous pull request.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we can follow up on it. This was a pre-existing invariant and my changes didn't eliminate the cases it would have applied to.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iitc if we get a relative path here we don't know what it would be relative too (CWD, PYTHONHOME or something else), so we chose to make this the responsibility of the caller. We're now passing sys_executable and sys_base_executable here, which are not enforced to be absolute, so we should change this from a panic to an error.

jtfmumm and others added 8 commits June 20, 2025 15:32
…Link` (#13953)

The `DirectorySymlink` struct assumes that it will be a Python minor
version link, so this renaming makes the intention clearer.
…anch (#13965)

The prerelease bump on `main` was lost when rebasing
`feature/transparent-python-upgrades` against it.
This PR:
* renames `PythonMinorVersionLink::symlink_exists()` to `..::exists`
* avoids rediscovering installations by chaining together new
installations with pre-existing installations when searching for the
highest patch per minor version key
* renames a couple of related errors and updates their messages to be
more specific
* renames `create_bin_link` to `create_link_to_executable`
* removes a constructor that is no longer used
(`PythonMinorVersinLink::from_interpreter()`)
This PR orders minor version requests and highest installation by minor
version keys by insertion order (using `indexmap::IndexSet` and
`IndexMap`). It also includes a couple of minor style updates.
…n` installation (#14084)

`uv python upgrade` had originally had a `--force` option, but we
thought it probably didn't make sense. However, switching `uv python
upgrade` behind the preview flag revealed a bad interaction: upgrading a
minor version when a non-managed interpreter of that version exists in
the `bin` directory displays an error recommending the use of `--force`.

This PR only warns if this case arises with `uv python upgrade`, noting
that install `--force` can be used. It also includes a test for the case
described above.
…ries (#14131)

For the preview version of transparent upgrades, this PR helps to more
clearly distinguish two features: (1) upgrading by installing the latest
patch version for a minor version and (2) supporting transparent
upgrades for things like virtual environments.

Transparently upgradeable installations now require `uv python install`
with the `--preview` flag. `uv python upgrade` will no longer create
symlink directories on its own, only point one to the latest patch if it
already exists.

This PR also changes `uv venv --preview` to no longer create a missing
symlink directory (a behavior that was meant to support a smooth
transition when upgrading uv, but which doesn't make sense in light of
the core change of this PR).
@jtfmumm jtfmumm force-pushed the feature/transparent-python-upgrades branch from 33adf2b to 4478db3 Compare June 20, 2025 13:32
@jtfmumm jtfmumm temporarily deployed to uv-test-registries June 20, 2025 13:34 — with GitHub Actions Inactive
@jtfmumm jtfmumm temporarily deployed to uv-test-registries June 20, 2025 13:42 — with GitHub Actions Inactive
@jtfmumm jtfmumm merged commit e9d5780 into main Jun 20, 2025
110 checks passed
@jtfmumm jtfmumm deleted the feature/transparent-python-upgrades branch June 20, 2025 14:17
@neutrinoceros
Copy link

Hi, I think I authored the oldest request for this feature (#7287) and I must say, I'm amazed by the amount of work that went into it. I had no idea it would be that involved, and I'm eternally grateful for your effort. Thank you so much !

@callegar
Copy link

Will this also be available for the tool subcommand that internally relies on virtualenvs. Specifically, will it be possible to say uv tool update -p <something>, or uv tool update -p <something> --all?

Even more interesting: in case you upgrade a managed python minor version (e.g uv python install 3.15.3 and then uv python uninstall 3.15.2) will it then be possible to have uv tool update --all update all the virtualenvs using the not-anymore-existing minor python version to the newer one?

jtfmumm added a commit that referenced this pull request Jun 21, 2025
jtfmumm added a commit that referenced this pull request Jun 23, 2025
…lations.find_all()` (#14180)

#13954 introduced an unnecessary slow-down to Python uninstall by
calling `installations.find_all()` to discover remaining installations
after an uninstall. Instead, we can filter all initial installations
against those in `uninstalled`.

As part of this change, I've updated `uninstalled` from a `Vec` to an
`IndexSet` in order to do efficient lookups in the filter. This required
a change I call out below to how we were retrieving them for messaging.
tmeijn pushed a commit to tmeijn/dotfiles that referenced this pull request Jun 24, 2025
This MR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [astral-sh/uv](https://github.com/astral-sh/uv) | patch | `0.7.13` -> `0.7.14` |

MR created with the help of [el-capitano/tools/renovate-bot](https://gitlab.com/el-capitano/tools/renovate-bot).

**Proposed changes to behavior should be submitted there as MRs.**

---

### Release Notes

<details>
<summary>astral-sh/uv (astral-sh/uv)</summary>

### [`v0.7.14`](https://github.com/astral-sh/uv/blob/HEAD/CHANGELOG.md#0714)

[Compare Source](astral-sh/uv@0.7.13...0.7.14)

##### Enhancements

- Add XPU to `--torch-backend` ([#&#8203;14172](astral-sh/uv#14172))
- Add ROCm backends to `--torch-backend` ([#&#8203;14120](astral-sh/uv#14120))
- Remove preview label from `--torch-backend` ([#&#8203;14119](astral-sh/uv#14119))
- Add `[tool.uv.dependency-groups].mygroup.requires-python` ([#&#8203;13735](astral-sh/uv#13735))
- Add auto-detection for AMD GPUs ([#&#8203;14176](astral-sh/uv#14176))
- Show retries for HTTP status code errors ([#&#8203;13897](astral-sh/uv#13897))
- Support transparent Python patch version upgrades ([#&#8203;13954](astral-sh/uv#13954))
- Warn on empty index directory ([#&#8203;13940](astral-sh/uv#13940))
- Publish to DockerHub ([#&#8203;14088](astral-sh/uv#14088))

##### Performance

- Make cold resolves about 10% faster ([#&#8203;14035](astral-sh/uv#14035))

##### Bug fixes

- Don't use walrus operator in interpreter query script ([#&#8203;14108](astral-sh/uv#14108))
- Fix handling of changes to `requires-python` ([#&#8203;14076](astral-sh/uv#14076))
- Fix implied `platform_machine` marker for `win_amd64` platform tag ([#&#8203;14041](astral-sh/uv#14041))
- Only update existing symlink directories on preview uninstall ([#&#8203;14179](astral-sh/uv#14179))
- Serialize Python requests for tools as canonicalized strings ([#&#8203;14109](astral-sh/uv#14109))
- Support netrc and same-origin credential propagation on index redirects ([#&#8203;14126](astral-sh/uv#14126))
- Support reading `dependency-groups` from pyproject.tomls with no `[project]` ([#&#8203;13742](astral-sh/uv#13742))
- Handle an existing shebang in `uv init --script` ([#&#8203;14141](astral-sh/uv#14141))
- Prevent concurrent updates of the environment in `uv run` ([#&#8203;14153](astral-sh/uv#14153))
- Filter managed Python distributions by platform before querying when included in request ([#&#8203;13936](astral-sh/uv#13936))

##### Documentation

- Replace cuda124 with cuda128 ([#&#8203;14168](astral-sh/uv#14168))
- Document the way member sources shadow workspace sources ([#&#8203;14136](astral-sh/uv#14136))
- Sync documented PyTorch integration index for CUDA and ROCm versions from PyTorch website ([#&#8203;14100](astral-sh/uv#14100))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever MR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this MR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box

---

This MR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiUmVub3ZhdGUgQm90Il19-->
jtfmumm added a commit that referenced this pull request Jun 25, 2025
jtfmumm added a commit that referenced this pull request Jun 25, 2025
#14252)

There was a regression introduced in #13954 on Windows where creating a
venv behaved as if there was a minor version link even if none existed.
This PR adds a check to fix this.

Closes #14249.
sai-rekhawar pushed a commit to sai-rekhawar/cloe-nessy-py that referenced this pull request Jul 1, 2025
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [ghcr.io/astral-sh/uv](https://github.com/astral-sh/uv) | final | patch | `0.7.13` -> `0.7.15` |

---

### Release Notes

<details>
<summary>astral-sh/uv (ghcr.io/astral-sh/uv)</summary>

### [`v0.7.15`](https://github.com/astral-sh/uv/blob/HEAD/CHANGELOG.md#0715)

[Compare Source](astral-sh/uv@0.7.14...0.7.15)

##### Enhancements

-   Consistently use `Ordering::Relaxed` for standalone atomic use cases ([#&#8203;14190](astral-sh/uv#14190))
-   Warn on ambiguous relative paths for `--index` ([#&#8203;14152](astral-sh/uv#14152))
-   Skip GitHub fast path when rate-limited ([#&#8203;13033](astral-sh/uv#13033))
-   Preserve newlines in `schema.json` descriptions ([#&#8203;13693](astral-sh/uv#13693))

##### Bug fixes

-   Add check for using minor version link when creating a venv on Windows ([#&#8203;14252](astral-sh/uv#14252))
-   Strip query parameters when parsing source URL ([#&#8203;14224](astral-sh/uv#14224))

##### Documentation

-   Add a link to PyPI FAQ to clarify what per-project token is ([#&#8203;14242](astral-sh/uv#14242))

##### Preview features

-   Allow symlinks in the build backend ([#&#8203;14212](astral-sh/uv#14212))

### [`v0.7.14`](https://github.com/astral-sh/uv/blob/HEAD/CHANGELOG.md#0714)

[Compare Source](astral-sh/uv@0.7.13...0.7.14)

##### Enhancements

-   Add XPU to `--torch-backend` ([#&#8203;14172](astral-sh/uv#14172))
-   Add ROCm backends to `--torch-backend` ([#&#8203;14120](astral-sh/uv#14120))
-   Remove preview label from `--torch-backend` ([#&#8203;14119](astral-sh/uv#14119))
-   Add `[tool.uv.dependency-groups].mygroup.requires-python` ([#&#8203;13735](astral-sh/uv#13735))
-   Add auto-detection for AMD GPUs ([#&#8203;14176](astral-sh/uv#14176))
-   Show retries for HTTP status code errors ([#&#8203;13897](astral-sh/uv#13897))
-   Support transparent Python patch version upgrades ([#&#8203;13954](astral-sh/uv#13954))
-   Warn on empty index directory ([#&#8203;13940](astral-sh/uv#13940))
-   Publish to DockerHub ([#&#8203;14088](astral-sh/uv#14088))

##### Performance

-   Make cold resolves about 10% faster ([#&#8203;14035](astral-sh/uv#14035))

##### Bug fixes

-   Don't use walrus operator in interpreter query script ([#&#8203;14108](astral-sh/uv#14108))
-   Fix handling of changes to `requires-python` ([#&#8203;14076](astral-sh/uv#14076))
-   Fix implied `platform_machine` marker for `...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or improvement to existing functionality preview Experimental behavior

Projects

None yet

6 participants