Repository: astral-sh/uv
Pull request #12168: Improve error message when a virtual environment Python symlink is broken
Author: login: konstin
Labels: name: error messages
description: Messaging when something goes wrong
Created: 2025-03-14T14:02:27Z | Merged: 2025-05-07T18:24:53Z

When removing a Python interpreter underneath an existing venv, uv currently shows a not found error:

```
error: Failed to inspect Python interpreter from active virtual environment at `.venv/bin/python3`
  Caused by: Python interpreter not found at `/home/konsti/projects/uv/.venv/bin/python3`
```

This is unintuitive, as the file for the Python interpreter does exist, it is a broken symlink that needs to be replaced with `uv venv`.

I've been encountering those occasionally, and I expect users that switch between versions a lot will, too, especially when they also use pyenv or a similar Python manager.

The new error hints at this solution:

```
error: Failed to inspect Python interpreter from active virtual environment at `.venv/bin/python3`
  Caused by: Broken symlink at `.venv/bin/python3`, was the underlying Python interpreter removed?

hint: To recreate the virtual environment, run `uv venv`
```


Diff:
diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs
index a77ae6c94e659..6add5eeae2957 100644
--- a/crates/uv-python/src/discovery.rs
+++ b/crates/uv-python/src/discovery.rs
@@ -35,7 +35,7 @@ use crate::virtualenv::{
 };
 #[cfg(windows)]
 use crate::windows_registry::{registry_pythons, WindowsPython};
-use crate::{Interpreter, PythonVersion};
+use crate::{BrokenSymlink, Interpreter, PythonVersion};
 
 /// A request to find a Python installation.
 ///
@@ -815,7 +815,8 @@ impl Error {
                     );
                     false
                 }
-                InterpreterError::NotFound(path) => {
+                InterpreterError::NotFound(path)
+                | InterpreterError::BrokenSymlink(BrokenSymlink { path, .. }) => {
                     // If the interpreter is from an active, valid virtual environment, we should
                     // fail because it's broken
                     if let Some(Ok(true)) = matches!(source, PythonSource::ActiveEnvironment)
@@ -894,11 +895,13 @@ pub fn find_python_installations<'a>(
                 debug!("Checking for Python interpreter at {request}");
                 match python_installation_from_executable(path, cache) {
                     Ok(installation) => Ok(Ok(installation)),
-                    Err(InterpreterError::NotFound(_)) => Ok(Err(PythonNotFound {
-                        request: request.clone(),
-                        python_preference: preference,
-                        environment_preference: environments,
-                    })),
+                    Err(InterpreterError::NotFound(_) | InterpreterError::BrokenSymlink(_)) => {
+                        Ok(Err(PythonNotFound {
+                            request: request.clone(),
+                            python_preference: preference,
+                            environment_preference: environments,
+                        }))
+                    }
                     Err(err) => Err(Error::Query(
                         Box::new(err),
                         path.clone(),
@@ -918,11 +921,13 @@ pub fn find_python_installations<'a>(
                 debug!("Checking for Python interpreter in {request}");
                 match python_installation_from_directory(path, cache) {
                     Ok(installation) => Ok(Ok(installation)),
-                    Err(InterpreterError::NotFound(_)) => Ok(Err(PythonNotFound {
-                        request: request.clone(),
-                        python_preference: preference,
-                        environment_preference: environments,
-                    })),
+                    Err(InterpreterError::NotFound(_) | InterpreterError::BrokenSymlink(_)) => {
+                        Ok(Err(PythonNotFound {
+                            request: request.clone(),
+                            python_preference: preference,
+                            environment_preference: environments,
+                        }))
+                    }
                     Err(err) => Err(Error::Query(
                         Box::new(err),
                         path.clone(),
diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs
index 42e3c8db71c19..bbc5595122dc9 100644
--- a/crates/uv-python/src/interpreter.rs
+++ b/crates/uv-python/src/interpreter.rs
@@ -677,6 +677,8 @@ impl Display for StatusCodeError {
 pub enum Error {
     #[error("Failed to query Python interpreter")]
     Io(#[from] io::Error),
+    #[error(transparent)]
+    BrokenSymlink(BrokenSymlink),
     #[error("Python interpreter not found at `{0}`")]
     NotFound(PathBuf),
     #[error("Failed to query Python interpreter at `{path}`")]
@@ -699,6 +701,33 @@ pub enum Error {
     Encode(#[from] rmp_serde::encode::Error),
 }
 
+#[derive(Debug, Error)]
+pub struct BrokenSymlink {
+    pub path: PathBuf,
+    /// Whether the interpreter path looks like a virtual environment.
+    pub venv: bool,
+}
+
+impl Display for BrokenSymlink {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "Broken symlink at `{}`, was the underlying Python interpreter removed?",
+            self.path.user_display()
+        )?;
+        if self.venv {
+            writeln!(
+                f,
+                "\n\n{}{} Consider recreating the environment (e.g., with `{}`)",
+                "hint".bold().cyan(),
+                ":".bold(),
+                "uv venv".green()
+            )?;
+        }
+        Ok(())
+    }
+}
+
 #[derive(Debug, Deserialize, Serialize)]
 #[serde(tag = "result", rename_all = "lowercase")]
 enum InterpreterInfoResult {
@@ -879,7 +908,24 @@ impl InterpreterInfo {
             .and_then(Timestamp::from_path)
             .map_err(|err| {
                 if err.kind() == io::ErrorKind::NotFound {
-                    Error::NotFound(executable.to_path_buf())
+                    // Check if it looks like a venv interpreter where the underlying Python
+                    // installation was removed.
+                    if absolute
+                        .symlink_metadata()
+                        .is_ok_and(|metadata| metadata.is_symlink())
+                    {
+                        let venv = executable
+                            .parent()
+                            .and_then(Path::parent)
+                            .map(|path| path.join("pyvenv.cfg").is_file())
+                            .unwrap_or(false);
+                        Error::BrokenSymlink(BrokenSymlink {
+                            path: executable.to_path_buf(),
+                            venv,
+                        })
+                    } else {
+                        Error::NotFound(executable.to_path_buf())
+                    }
                 } else {
                     err.into()
                 }
diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs
index ca723db6daaf1..07612a58954a7 100644
--- a/crates/uv-python/src/lib.rs
+++ b/crates/uv-python/src/lib.rs
@@ -11,7 +11,7 @@ pub use crate::discovery::{
 pub use crate::environment::{InvalidEnvironmentKind, PythonEnvironment};
 pub use crate::implementation::ImplementationName;
 pub use crate::installation::{PythonInstallation, PythonInstallationKey};
-pub use crate::interpreter::{Error as InterpreterError, Interpreter};
+pub use crate::interpreter::{BrokenSymlink, Error as InterpreterError, Interpreter};
 pub use crate::pointer_size::PointerSize;
 pub use crate::prefix::Prefix;
 pub use crate::python_version::PythonVersion;
diff --git a/crates/uv-tool/src/lib.rs b/crates/uv-tool/src/lib.rs
index fbaca618ee180..a7a7cfb1b5217 100644
--- a/crates/uv-tool/src/lib.rs
+++ b/crates/uv-tool/src/lib.rs
@@ -227,19 +227,22 @@ impl InstalledTools {
             Err(uv_python::Error::Query(uv_python::InterpreterError::NotFound(
                 interpreter_path,
             ))) => {
-                if interpreter_path.is_symlink() {
-                    let target_path = fs_err::read_link(&interpreter_path)?;
-                    warn!(
-                        "Ignoring existing virtual environment linked to non-existent Python interpreter: {} -> {}",
-                        interpreter_path.user_display(),
-                        target_path.user_display()
-                    );
-                } else {
-                    warn!(
-                        "Ignoring existing virtual environment with missing Python interpreter: {}",
-                        interpreter_path.user_display()
-                    );
-                }
+                warn!(
+                    "Ignoring existing virtual environment with missing Python interpreter: {}",
+                    interpreter_path.user_display()
+                );
+
+                Ok(None)
+            }
+            Err(uv_python::Error::Query(uv_python::InterpreterError::BrokenSymlink(
+                broken_symlink,
+            ))) => {
+                let target_path = fs_err::read_link(&broken_symlink.path)?;
+                warn!(
+                    "Ignoring existing virtual environment linked to non-existent Python interpreter: {} -> {}",
+                    broken_symlink.path.user_display(),
+                    target_path.user_display()
+                );
 
                 Ok(None)
             }
diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs
index 4c290d5d24deb..70eedb0f6c45f 100644
--- a/crates/uv/src/commands/project/mod.rs
+++ b/crates/uv/src/commands/project/mod.rs
@@ -914,15 +914,16 @@ impl ProjectInterpreter {
                     InvalidEnvironmentKind::Empty => {}
                 }
             }
-            Err(uv_python::Error::Query(uv_python::InterpreterError::NotFound(path))) => {
-                if path.is_symlink() {
-                    let target_path = fs_err::read_link(&path)?;
-                    warn_user!(
-                        "Ignoring existing virtual environment linked to non-existent Python interpreter: {} -> {}",
-                        path.user_display().cyan(),
-                        target_path.user_display().cyan(),
-                    );
-                }
+            Err(uv_python::Error::Query(uv_python::InterpreterError::NotFound(_))) => {}
+            Err(uv_python::Error::Query(uv_python::InterpreterError::BrokenSymlink(
+                broken_symlink,
+            ))) => {
+                let target_path = fs_err::read_link(&broken_symlink.path)?;
+                warn_user!(
+                    "Ignoring existing virtual environment linked to non-existent Python interpreter: {} -> {}",
+                    broken_symlink.path.user_display().cyan(),
+                    target_path.user_display().cyan(),
+                );
             }
             Err(err) => return Err(err.into()),
         }
diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs
index f3655b58f6ae4..cc0c95f90ba9d 100644
--- a/crates/uv/tests/it/pip_compile.rs
+++ b/crates/uv/tests/it/pip_compile.rs
@@ -17360,3 +17360,46 @@ fn incompatible_cuda() -> Result<()> {
 
     Ok(())
 }
+
+#[cfg(unix)]
+#[test]
+fn compile_broken_active_venv() -> Result<()> {
+    let context = TestContext::new("3.12");
+    let requirements_in = context.temp_dir.child("requirements.in");
+    requirements_in.write_str("anyio==3.7.0")?;
+
+    // A broken system Python
+    let broken_system_python = context.temp_dir.join("python3.14159");
+    fs_err::os::unix::fs::symlink("/does/not/exist", &broken_system_python)?;
+    uv_snapshot!(context
+        .venv()
+        .arg("--python")
+        .arg(&broken_system_python)
+        .arg("venv2"), @r"
+    success: false
+    exit_code: 1
+    ----- stdout -----
+
+    ----- stderr -----
+      × No interpreter found for path `python3.14159` in managed installations or search path
+    ");
+
+    // Simulate a removed Python interpreter
+    fs_err::remove_file(context.interpreter())?;
+    fs_err::os::unix::fs::symlink("/removed/python/interpreter", context.interpreter())?;
+    uv_snapshot!(context
+        .pip_compile()
+        .arg("requirements.in"), @r"
+    success: false
+    exit_code: 2
+    ----- stdout -----
+
+    ----- stderr -----
+    error: Failed to inspect Python interpreter from active virtual environment at `.venv/bin/python3`
+      Caused by: Broken symlink at `.venv/bin/python3`, was the underlying Python interpreter removed?
+
+    hint: Consider recreating the environment (e.g., with `uv venv`)
+    ");
+
+    Ok(())
+}


Reviews:
user: login: zanieb
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T16:45:56Z
user: login: zanieb
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T16:46:43Z
user: login: zanieb
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T16:48:48Z
user: login: konstin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T22:35:39Z
user: login: zanieb
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T22:40:21Z
user: login: konstin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T22:44:57Z
user: login: konstin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T22:52:43Z
user: login: zanieb
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T23:29:00Z
user: login: zanieb
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T17:28:38Z
user: login: zanieb
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-07T17:58:42Z
user: login: zanieb
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T18:06:05Z
user: login: konstin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T18:07:44Z
user: login: zanieb
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T18:10:34Z

Review comments:
diff_hunk: @@ -899,11 +901,13 @@ pub fn find_python_installations<'a>(
                 debug!("Checking for Python interpreter in {request}");
                 match python_installation_from_directory(path, cache) {
                     Ok(installation) => Ok(Ok(installation)),
-                    Err(InterpreterError::NotFound(_)) => Ok(Err(PythonNotFound {
-                        request: request.clone(),
-                        python_preference: preference,
-                        environment_preference: environments,
-                    })),
+                    Err(InterpreterError::NotFound(_) | InterpreterError::BrokenSymlink(_)) => {
path: crates/uv-python/src/discovery.rs
user: login: zanieb
body: Isn't it important to report the broken symlink in this context too? I'm not sure how that'd look downstream. You could test with `uv venv foo && <uninstall python> && uv run -p foo`?
created_at: 2025-04-16T16:45:56Z
author_association: MEMBER
diff_hunk: @@ -899,11 +901,13 @@ pub fn find_python_installations<'a>(
                 debug!("Checking for Python interpreter in {request}");
                 match python_installation_from_directory(path, cache) {
                     Ok(installation) => Ok(Ok(installation)),
-                    Err(InterpreterError::NotFound(_)) => Ok(Err(PythonNotFound {
-                        request: request.clone(),
-                        python_preference: preference,
-                        environment_preference: environments,
-                    })),
+                    Err(InterpreterError::NotFound(_) | InterpreterError::BrokenSymlink(_)) => {
path: crates/uv-python/src/discovery.rs
user: login: zanieb
body: I guess the hint could be wrong here if it's not a virtual environment?
created_at: 2025-04-16T16:46:43Z
author_association: MEMBER
diff_hunk: @@ -15402,3 +15402,31 @@ fn respect_non_local_preference() -> Result<()> {
 
     Ok(())
 }
+
+#[cfg(unix)]
+#[test]
+fn compile_broken_active_venv() -> Result<()> {
+    let context = TestContext::new("3.12");
+    let requirements_in = context.temp_dir.child("requirements.in");
+    requirements_in.write_str("anyio==3.7.0")?;
+
+    // Simulate a removed Python interpreter
+    fs_err::remove_file(context.interpreter())?;
+    std::os::unix::fs::symlink("/removed/python/interpreter", context.interpreter())?;
+
+    uv_snapshot!(context
+        .pip_compile()
+        .arg("requirements.in"), @r"
+    success: false
+    exit_code: 2
+    ----- stdout -----
+
+    ----- stderr -----
+    error: Failed to inspect Python interpreter from active virtual environment at `.venv/bin/python3`
+      Caused by: Broken symlink at `.venv/bin/python3`, was the underlying Python interpreter removed?
+
+    hint: To recreate the virtual environment, run `uv venv`
path: crates/uv/tests/it/pip_compile.rs
user: login: zanieb
body: What if the virtual environment was found in a parent directory or specified some other way? I'm worried this hint might be misleading.
created_at: 2025-04-16T16:48:48Z
author_association: MEMBER
diff_hunk: @@ -15402,3 +15402,31 @@ fn respect_non_local_preference() -> Result<()> {
 
     Ok(())
 }
+
+#[cfg(unix)]
+#[test]
+fn compile_broken_active_venv() -> Result<()> {
+    let context = TestContext::new("3.12");
+    let requirements_in = context.temp_dir.child("requirements.in");
+    requirements_in.write_str("anyio==3.7.0")?;
+
+    // Simulate a removed Python interpreter
+    fs_err::remove_file(context.interpreter())?;
+    std::os::unix::fs::symlink("/removed/python/interpreter", context.interpreter())?;
+
+    uv_snapshot!(context
+        .pip_compile()
+        .arg("requirements.in"), @r"
+    success: false
+    exit_code: 2
+    ----- stdout -----
+
+    ----- stderr -----
+    error: Failed to inspect Python interpreter from active virtual environment at `.venv/bin/python3`
+      Caused by: Broken symlink at `.venv/bin/python3`, was the underlying Python interpreter removed?
+
+    hint: To recreate the virtual environment, run `uv venv`
path: crates/uv/tests/it/pip_compile.rs
user: login: konstin
body: Should we make the hint more generic, like "hint: The Python interpreter seems to have been uninstalled, consider recreating the virtual environment"?
created_at: 2025-04-16T22:35:39Z
author_association: MEMBER
diff_hunk: @@ -15402,3 +15402,31 @@ fn respect_non_local_preference() -> Result<()> {
 
     Ok(())
 }
+
+#[cfg(unix)]
+#[test]
+fn compile_broken_active_venv() -> Result<()> {
+    let context = TestContext::new("3.12");
+    let requirements_in = context.temp_dir.child("requirements.in");
+    requirements_in.write_str("anyio==3.7.0")?;
+
+    // Simulate a removed Python interpreter
+    fs_err::remove_file(context.interpreter())?;
+    std::os::unix::fs::symlink("/removed/python/interpreter", context.interpreter())?;
+
+    uv_snapshot!(context
+        .pip_compile()
+        .arg("requirements.in"), @r"
+    success: false
+    exit_code: 2
+    ----- stdout -----
+
+    ----- stderr -----
+    error: Failed to inspect Python interpreter from active virtual environment at `.venv/bin/python3`
+      Caused by: Broken symlink at `.venv/bin/python3`, was the underlying Python interpreter removed?
+
+    hint: To recreate the virtual environment, run `uv venv`
path: crates/uv/tests/it/pip_compile.rs
user: login: zanieb
body: That seems more correct. You could sniff for `pyenv.cfg` too? If this was handled in the caller instead (as it sort of was before this PR?) you would have information about whether it's a virtual environment or not?
created_at: 2025-04-16T22:40:21Z
author_association: MEMBER
diff_hunk: @@ -899,11 +901,13 @@ pub fn find_python_installations<'a>(
                 debug!("Checking for Python interpreter in {request}");
                 match python_installation_from_directory(path, cache) {
                     Ok(installation) => Ok(Ok(installation)),
-                    Err(InterpreterError::NotFound(_)) => Ok(Err(PythonNotFound {
-                        request: request.clone(),
-                        python_preference: preference,
-                        environment_preference: environments,
-                    })),
+                    Err(InterpreterError::NotFound(_) | InterpreterError::BrokenSymlink(_)) => {
path: crates/uv-python/src/discovery.rs
user: login: konstin
body: For this case, I get the same behavior with and without this PR:

```
$ uv venv botched && rm botched/bin/python && ln -s /does/not/exist/python botched/bin/python && uv run -p botched
Using CPython 3.13.2
Creating virtual environment at: botched
Activate with: source botched/bin/activate
error: No interpreter found for directory `botched` in virtual environments, managed installations, or search path
$ uv-pr venv botched && rm botched/bin/python && ln -s /does/not/exist/python botched/bin/python && uv-pr run -p botched
Using CPython 3.13.2
Creating virtual environment at: botched
Activate with: source botched/bin/activate
error: No interpreter found for directory `botched` in virtual environments, managed installations, or search path
```

The error is imo misleading, we found an interpreter, but it's broken, though that looks like a different problem. 
created_at: 2025-04-16T22:44:57Z
author_association: MEMBER
diff_hunk: @@ -15402,3 +15402,31 @@ fn respect_non_local_preference() -> Result<()> {
 
     Ok(())
 }
+
+#[cfg(unix)]
+#[test]
+fn compile_broken_active_venv() -> Result<()> {
+    let context = TestContext::new("3.12");
+    let requirements_in = context.temp_dir.child("requirements.in");
+    requirements_in.write_str("anyio==3.7.0")?;
+
+    // Simulate a removed Python interpreter
+    fs_err::remove_file(context.interpreter())?;
+    std::os::unix::fs::symlink("/removed/python/interpreter", context.interpreter())?;
+
+    uv_snapshot!(context
+        .pip_compile()
+        .arg("requirements.in"), @r"
+    success: false
+    exit_code: 2
+    ----- stdout -----
+
+    ----- stderr -----
+    error: Failed to inspect Python interpreter from active virtual environment at `.venv/bin/python3`
+      Caused by: Broken symlink at `.venv/bin/python3`, was the underlying Python interpreter removed?
+
+    hint: To recreate the virtual environment, run `uv venv`
path: crates/uv/tests/it/pip_compile.rs
user: login: konstin
body: @jtfmumm you got an opinion on that from the other work on healing broken venvs wrt `pyvenv.cfg`?
created_at: 2025-04-16T22:52:43Z
author_association: MEMBER
diff_hunk: @@ -15402,3 +15402,31 @@ fn respect_non_local_preference() -> Result<()> {
 
     Ok(())
 }
+
+#[cfg(unix)]
+#[test]
+fn compile_broken_active_venv() -> Result<()> {
+    let context = TestContext::new("3.12");
+    let requirements_in = context.temp_dir.child("requirements.in");
+    requirements_in.write_str("anyio==3.7.0")?;
+
+    // Simulate a removed Python interpreter
+    fs_err::remove_file(context.interpreter())?;
+    std::os::unix::fs::symlink("/removed/python/interpreter", context.interpreter())?;
+
+    uv_snapshot!(context
+        .pip_compile()
+        .arg("requirements.in"), @r"
+    success: false
+    exit_code: 2
+    ----- stdout -----
+
+    ----- stderr -----
+    error: Failed to inspect Python interpreter from active virtual environment at `.venv/bin/python3`
+      Caused by: Broken symlink at `.venv/bin/python3`, was the underlying Python interpreter removed?
+
+    hint: To recreate the virtual environment, run `uv venv`
path: crates/uv/tests/it/pip_compile.rs
user: login: zanieb
body: i.e., we have https://github.com/astral-sh/uv/blob/7d12b6af00fea7b45d0c0e1e2d0f9431b3cf9b9c/crates/uv-python/src/discovery.rs#L819-L828
created_at: 2025-04-16T23:29:00Z
author_association: MEMBER
diff_hunk: @@ -677,6 +677,14 @@ impl Display for StatusCodeError {
 pub enum Error {
     #[error("Failed to query Python interpreter")]
     Io(#[from] io::Error),
+    #[error(
+        "Broken symlink at `{}`, was the underlying Python interpreter removed?\n\n{}{} Consider recreating the environment (e.g., with `{}`)",
+        _0.user_display(),
+        "hint".bold().cyan(),
+        ":".bold(),
+        "uv venv".green(),
+    )]
+    BrokenVenvSymlink(PathBuf),
path: crates/uv-python/src/interpreter.rs
user: login: zanieb
body: Doesn't specializing this case mean we're not improving the error message when it's not a virtual environment? I'm curious why you chose this instead of having conditional display of the hint? Either with `BrokenSymlink(PathBuf, bool)` or by deferring that `pyvenv.cfg` lookup to error display time. (adding another dedicated variant is an option, of course, but seems like more work downstream of here)
created_at: 2025-05-07T17:28:38Z
author_association: MEMBER
diff_hunk: @@ -17368,10 +17368,25 @@ fn compile_broken_active_venv() -> Result<()> {
     let requirements_in = context.temp_dir.child("requirements.in");
     requirements_in.write_str("anyio==3.7.0")?;
 
+    // A broken system Python
+    let broken_system_python = context.temp_dir.join("python3.14159");
+    fs_err::os::unix::fs::symlink("/does/not/exist", &broken_system_python)?;
+    uv_snapshot!(context
+        .venv()
+        .arg("--python")
+        .arg(&broken_system_python)
+        .arg("venv2"), @r"
+    success: false
+    exit_code: 1
+    ----- stdout -----
+
+    ----- stderr -----
+      × No interpreter found for path `python3.14159` in managed installations or search path
path: crates/uv/tests/it/pip_compile.rs
user: login: zanieb
body: The specialization for that would be at

https://github.com/astral-sh/uv/blob/060be9cef197d83e5546fb1be10e8e8373a1cfc6/crates/uv-python/src/discovery.rs#L897-L906

It's not great right now. I think we should tackle that separately though.
created_at: 2025-05-07T18:06:05Z
author_association: MEMBER
diff_hunk: @@ -677,6 +677,14 @@ impl Display for StatusCodeError {
 pub enum Error {
     #[error("Failed to query Python interpreter")]
     Io(#[from] io::Error),
+    #[error(
+        "Broken symlink at `{}`, was the underlying Python interpreter removed?\n\n{}{} Consider recreating the environment (e.g., with `{}`)",
+        _0.user_display(),
+        "hint".bold().cyan(),
+        ":".bold(),
+        "uv venv".green(),
+    )]
+    BrokenVenvSymlink(PathBuf),
path: crates/uv-python/src/interpreter.rs
user: login: konstin
body: I've only seen the error show in uv for venvs, but it doesn't hurt to cover the non-venv case, too.

In the test the error looks suboptimal too (we're ignoring the explicitly passed python interpreter is unexpected, I checked both `uv venv` and `uv pip compile`), but that's a different problem (and the behavior for this case does not change).
created_at: 2025-05-07T18:07:44Z
author_association: MEMBER
diff_hunk: @@ -17368,10 +17368,25 @@ fn compile_broken_active_venv() -> Result<()> {
     let requirements_in = context.temp_dir.child("requirements.in");
     requirements_in.write_str("anyio==3.7.0")?;
 
+    // A broken system Python
+    let broken_system_python = context.temp_dir.join("python3.14159");
+    fs_err::os::unix::fs::symlink("/does/not/exist", &broken_system_python)?;
+    uv_snapshot!(context
+        .venv()
+        .arg("--python")
+        .arg(&broken_system_python)
+        .arg("venv2"), @r"
+    success: false
+    exit_code: 1
+    ----- stdout -----
+
+    ----- stderr -----
+      × No interpreter found for path `python3.14159` in managed installations or search path
path: crates/uv/tests/it/pip_compile.rs
user: login: zanieb
body: Looking at an aspect of this briefly... https://github.com/astral-sh/uv/pull/13335
created_at: 2025-05-07T18:10:34Z
author_association: MEMBER

PR comments:
user: login: charliermarsh
created_at: 2025-03-14T14:08:57Z
body: Nice. Can we add another newline before the hint, and remove the trailing period on the hint per the style guide?
author_association: MEMBER
user: login: konstin
created_at: 2025-03-14T14:18:42Z
body: I'm considering removing `InterpreterError::NotFound`, do we have cases we want to permit that are not covered by `InterpreterError::BrokenSymlink`?
author_association: MEMBER
user: login: zanieb
created_at: 2025-04-16T16:44:44Z
body: > I'm considering removing InterpreterError::NotFound

I think we need it for a case like `uv run -p /path/to/python3.12`, right? A missing file there is different than a broken symlink
author_association: MEMBER
user: login: konstin
created_at: 2025-04-16T22:32:17Z
body: > > I'm considering removing InterpreterError::NotFound
> 
> I think we need it for a case like `uv run -p /path/to/python3.12`, right? A missing file there is different than a broken symlink

I'm afraid I'm not following along what `/path/to/python3.12` means. If I do `uv run -p /usr/bin/python3.12 python` it works in either case. If I do `uv run -p /does/not/exist/python3.12 python` there's the current behavior and the one if we remove `InterpreterError::NotFound`.

Current behavior:

```
$ uv run -p /does/not/exist/python3.12 python
error: No interpreter found for path `/does/not/exist/python3.12` in virtual environments, managed installations, or search path
```

After removing `InterpreterError::NotFound`:

```
$ uv run -p /does/not/exist/python3.12 python
error: Failed to inspect Python interpreter from provided path at `/does/not/exist/python3.12`
  Caused by: Failed to query Python interpreter
  Caused by: failed to canonicalize path `/does/not/exist/python3.12`: No such file or directory (os error 2)
```
author_association: MEMBER
user: login: zanieb
created_at: 2025-04-16T22:39:04Z
body: I see. I think that might cause other problems for discovery, I'd need to look carefully at the downstream effects. I can do that though..
author_association: MEMBER
user: login: konstin
created_at: 2025-04-16T22:55:50Z
body: It would be interesting to know if this has downstream effects, it would be nice if we could scope the special case down to only broken symlinks, which tightly corresponds to uninstalled python interpreters.
author_association: MEMBER
user: login: konstin
created_at: 2025-05-06T11:20:47Z
body: @zanieb Could you find out if we still need `InterpreterError::NotFound` with `InterpreterError::BrokenSymlink`, or if there are downstream problems?
author_association: MEMBER
user: login: zanieb
created_at: 2025-05-06T12:27:34Z
body: I can try, but it's going to be hard to find time as it's not very critical. Do we need to remove it? It's kind of high risk to remove error variants that are used in Python discovery.
author_association: MEMBER
user: login: konstin
created_at: 2025-05-06T14:03:43Z
body: If we don't have the clarity about `InterpreterError::NotFound`, can we move ahead with just adding `InterpreterError::BrokenSymlink`?
author_association: MEMBER
user: login: zanieb
created_at: 2025-05-07T17:58:51Z
body: Thanks for your patience
author_association: MEMBER