# How uv Discovers Project Configuration Files Like pyproject.toml

> Learn how uv discovers project configuration files like pyproject.toml by walking up directories. Understand workspace roots, inclusion globs, and validation rules for efficient project management.

- Repository: [Astral/uv](https://github.com/astral-sh/uv)
- Tags: internals
- Published: 2026-03-01

---

**uv discovers project configuration by walking up the directory tree from the current working directory to locate the nearest [`pyproject.toml`](https://github.com/astral-sh/uv/blob/main/pyproject.toml) containing a `[project]` table, then optionally continues upward to find a workspace root defined by `tool.uv.workspace`, finally assembling a `Workspace` object that respects inclusion globs, exclusion patterns, and validation rules.**

The `uv` package manager from Astral implements a deterministic, filesystem-based discovery mechanism to locate and load Python project configurations. When executing commands from any subdirectory, uv must resolve which [`pyproject.toml`](https://github.com/astral-sh/uv/blob/main/pyproject.toml) defines the current project and whether that project belongs to a larger workspace. This resolution process, implemented primarily in [`crates/uv-workspace/src/workspace.rs`](https://github.com/astral-sh/uv/blob/main/crates/uv-workspace/src/workspace.rs), combines ancestor directory traversal with TOML parsing and glob-based membership testing.

## The Three-Step Discovery Process

uv's configuration discovery operates through three coordinated phases, each handled by specific functions in the workspace crate.

### Step 1: Locating the Nearest Project Root

The discovery begins with `ProjectWorkspace::discover` (lines 1681-1689 in [`crates/uv-workspace/src/workspace.rs`](https://github.com/astral-sh/uv/blob/main/crates/uv-workspace/src/workspace.rs)). Starting from the current working directory, uv climbs the filesystem tree using `path.ancestors()`, stopping at the first directory containing a [`pyproject.toml`](https://github.com/astral-sh/uv/blob/main/pyproject.toml) file that includes a `[project]` table.

```rust
let project_root = path
    .ancestors()
    .take_while(|p| { /* stop_discovery_at handling */ })
    .find(|p| p.join("pyproject.toml").is_file())
    .ok_or(WorkspaceError::MissingPyprojectToml)?;

```

Once found, uv reads and parses the file using `PyProjectToml::from_string` (defined in [`crates/uv-workspace/src/pyproject.rs`](https://github.com/astral-sh/uv/blob/main/crates/uv-workspace/src/pyproject.rs)), extracting the project metadata.

### Step 2: Searching for an Explicit Workspace

After locating the project root, uv continues upward via `find_workspace` (line 1491) to detect if the project belongs to a larger workspace. The function iterates through `project_root.ancestors().skip(1)`, examining each ancestor directory for a [`pyproject.toml`](https://github.com/astral-sh/uv/blob/main/pyproject.toml) containing a `tool.uv.workspace` table.

When a potential workspace root is found, uv validates membership using `is_included_in_workspace` and `is_excluded_from_workspace` (lines 1529-1544). The project must match the workspace's `members` glob patterns and not match any `exclude` patterns. If excluded, uv returns `Ok(None)` at line 1537, treating the project as standalone.

### Step 3: Assembling the Full Workspace

If a valid workspace root exists, `Workspace::discover` (line 1869) orchestrates the final assembly. This process calls `collect_members` (lines 3340-3430), which delegates to `collect_members_only` to expand workspace globs and validate each member.

The member collection process (lines 5115-5210) performs several validations:
- Each member must contain a [`pyproject.toml`](https://github.com/astral-sh/uv/blob/main/pyproject.toml) with a `[project]` table
- Members cannot specify `managed = false` (line 5110)
- Package names must be unique across the workspace (line 5148)
- Nested workspaces are prohibited (line 5245)

If no explicit workspace is found, uv creates an **implicit single-package workspace** where the project root serves as both the project and workspace root.

## Edge Cases and Validation Rules

uv handles several edge cases during discovery, each with specific error handling:

- **Missing [`pyproject.toml`](https://github.com/astral-sh/uv/blob/main/pyproject.toml)**: Returns `WorkspaceError::MissingPyprojectToml` when no configuration file exists in the ancestor chain.
- **Missing `[project]` table**: Logs a warning (line 1578) and treats the directory as a non-project if neither `[project]` nor `tool.uv.workspace` exists.
- **Excluded members**: Silently skips projects matching workspace `exclude` globs (line 1537).
- **Missing member [`pyproject.toml`](https://github.com/astral-sh/uv/blob/main/pyproject.toml)**: Returns `MissingPyprojectTomlMember` (line 5162) if a glob matches a directory without a configuration file.
- **Duplicate package names**: Returns `DuplicatePackage` (line 5148) if two members share the same package name.
- **Nested workspaces**: Returns `NestedWorkspace` (line 5245) if a member defines its own `tool.uv.workspace`.
- **Unmanaged projects**: Skips projects with `managed = false` (line 5110), excluding them from the workspace.

## Practical Examples

### CLI Usage

When running uv commands from any subdirectory, automatic discovery occurs without explicit flags:

```bash

# From a deeply nested package directory

$ uv pip install requests

```

uv automatically locates the nearest [`pyproject.toml`](https://github.com/astral-sh/uv/blob/main/pyproject.toml), determines workspace membership, and resolves dependencies according to the discovered configuration.

### Programmatic Discovery in Rust

To implement custom tooling around uv's discovery mechanism:

```rust
use uv_workspace::{ProjectWorkspace, DiscoveryOptions, WorkspaceCache};

#[tokio::main]
async fn main() -> Result<(), uv_workspace::WorkspaceError> {
    // Configure discovery to stop at a specific ancestor
    let opts = DiscoveryOptions {
        stop_discovery_at: Some(std::path::PathBuf::from("/my/repo")),
        ..Default::default()
    };

    // Initialize cache for workspace resolution
    let cache = WorkspaceCache::default();

    // Discover from current directory
    let proj_ws = ProjectWorkspace::discover(
        std::env::current_dir()?.as_path(), 
        &opts, 
        &cache
    ).await?;

    println!("Project root: {}", proj_ws.project_root.display());
    println!("Workspace root: {}", proj_ws.workspace.install_path().display());

    // Iterate workspace members
    for (name, member) in proj_ws.workspace.packages() {
        println!("Member: {} → {}", name, member.root().display());
    }

    Ok(())
}

```

### Inspecting Parsed Configuration

To examine the parsed [`pyproject.toml`](https://github.com/astral-sh/uv/blob/main/pyproject.toml) structure:

```rust
use uv_workspace::pyproject::PyProjectToml;

let toml_str = std::fs::read_to_string("pyproject.toml")?;
let pyproject: PyProjectToml = PyProjectToml::from_string(toml_str)?;
println!("Project name: {}", pyproject.project.unwrap().name);

```

## Summary

- uv discovers project configuration through a three-phase process: locating the nearest project root, searching for an explicit workspace ancestor, and assembling the complete workspace structure.
- The discovery implementation resides primarily in [`crates/uv-workspace/src/workspace.rs`](https://github.com/astral-sh/uv/blob/main/crates/uv-workspace/src/workspace.rs), with key entry points at `ProjectWorkspace::discover` (line 1681) and `Workspace::discover` (line 1869).
- Workspace membership is determined by glob patterns defined in `tool.uv.workspace.members` and `tool.uv.workspace.exclude`, validated through `is_included_in_workspace` and `is_excluded_from_workspace`.
- The system handles edge cases including missing configuration files, duplicate package names, nested workspaces, and unmanaged projects with specific error types and warnings.

## Frequently Asked Questions

### How does uv handle multiple pyproject.toml files in parent directories?

uv climbs the directory tree using `path.ancestors()` until it finds the first [`pyproject.toml`](https://github.com/astral-sh/uv/blob/main/pyproject.toml) containing a `[project]` table, which becomes the project root. If searching for a workspace, it continues upward to find a [`pyproject.toml`](https://github.com/astral-sh/uv/blob/main/pyproject.toml) with `tool.uv.workspace`, but skips the already-found project directory to avoid self-reference.

### What happens if a project is excluded from a workspace?

When uv discovers a potential workspace root, it checks if the project is included in the workspace's `members` globs and not excluded by `exclude` globs using `is_included_in_workspace` and `is_excluded_from_workspace` (lines 1529-1544). If excluded, uv returns `Ok(None)` at line 1537 and treats the project as a standalone single-package workspace rather than part of the larger workspace.

### Can uv discover projects with a pyproject.toml but no [project] table?

If a [`pyproject.toml`](https://github.com/astral-sh/uv/blob/main/pyproject.toml) lacks both a `[project]` table and a `tool.uv.workspace` definition, uv logs a warning at line 1578 and treats the directory as a non-project. For workspace member discovery, uv explicitly requires the `[project]` table and returns `MissingProject` errors if it's absent (around line 5148 context).

### How does uv prevent nested workspaces?

During member collection in `collect_members_only` (lines 5115-5210), uv checks each member's [`pyproject.toml`](https://github.com/astral-sh/uv/blob/main/pyproject.toml) for the presence of `tool.uv.workspace`. If a member defines its own workspace, uv returns a `NestedWorkspace` error at line 5245, preventing recursive workspace definitions that would create ambiguous ownership hierarchies.