Workspaces, Virtual Environments, and Dependency Isolation

Updated

September 7, 2026

Workspaces, Virtual Environments, and Dependency Isolation

After reading this chapter, you will master the internal mechanics of Python virtual environments (pyvenv.cfg and site-packages), manage modern multi-package monorepo workspaces using uv, author declarative pyproject.toml project files, and run self-contained scripts using PEP 723 inline dependency metadata.

Mental model

A Python virtual environment is not a virtual machine or a container. It is an isolated directory tree containing symlinks to a base Python interpreter and a dedicated site-packages directory.

Virtual Environment Architecture (.venv/):
  .venv/
  ├── bin/
  │   ├── python ──▶ Symlink to base CPython binary
  │   └── uv
  ├── pyvenv.cfg  ──▶ Configuration: home = /usr/bin, version = 3.14.0
  └── lib/python3.14/
      └── site-packages/ ──▶ Isolated third-party libraries installed here

When CPython starts: 1. It searches upward from its executable for pyvenv.cfg. 2. If found, it sets sys.prefix to the .venv folder, while keeping sys.base_prefix pointing to the host Python installation. 3. Third-party package imports look exclusively inside the .venv’s site-packages directory.


Minimal example

Save as venv_internals.py:

# venv_internals.py
import sys
import site
from pathlib import Path

def inspect_environment() -> None:
    is_virtual_env = sys.prefix != sys.base_prefix

    print(f"Is running inside a virtual environment? {is_virtual_env}")
    print(f"Base CPython runtime (sys.base_prefix) : {sys.base_prefix}")
    print(f"Active environment root (sys.prefix)    : {sys.prefix}")
    print(f"Active Python executable (sys.executable): {sys.executable}")

    # Inspect site-packages search paths
    site_packages = site.getsitepackages()
    print("\nActive site-packages directories:")
    for sp in site_packages:
        print(f"  - {sp}")

def main() -> None:
    inspect_environment()

if __name__ == "__main__":
    main()

Run via uv run python venv_internals.py:

Is running inside a virtual environment? True
Base CPython runtime (sys.base_prefix) : ...
Active environment root (sys.prefix)    : .../.venv
Active Python executable (sys.executable): .../.venv/bin/python

Active site-packages directories:
  - .../.venv/lib/python3.14/site-packages

Worked examples

Case 1: Inline Script Dependencies (PEP 723)

In automation, CI/CD, and DevOps scripts, creating a separate pyproject.toml and .venv for a single 50-line script adds unnecessary friction. PEP 723 allows embedding dependency requirements directly inside the script header:

# generate_qr_matrix.py
# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "rich>=13.0.0",
# ]
# ///
import sys
from rich.console import Console
from rich.table import Table

def display_cluster_health() -> None:
    console = Console()
    table = Table(title="Production Node Health")
    table.add_column("Node", style="cyan")
    table.add_column("Status", style="green")
    table.add_column("Load", style="magenta")

    table.add_row("k8s-node-01", "READY", "12.4%")
    table.add_row("k8s-node-02", "READY", "8.9%")
    table.add_row("k8s-node-03", "DRAINING", "94.2%")

    console.print(table)

if __name__ == "__main__":
    display_cluster_health()

When you execute:

uv run generate_qr_matrix.py

uv automatically: 1. Parses the # /// script metadata block. 2. Creates an ephemeral, cached virtual environment containing rich. 3. Executes the script inside the isolated environment with zero pollution of the host system!

Case 2: Multi-Package Workspaces with uv

In modern monorepos (such as a backend repository containing shared utilities, an API service, and a background worker), uv supports multi-package workspaces configured via pyproject.toml:

# pyproject.toml (Monorepo root)
[tool.uv.workspace]
members = ["packages/*", "services/*"]

[package]
name = "enterprise-monorepo"
version = "0.1.0"

Directory Structure:

monorepo/
├── pyproject.toml
├── uv.lock                    ──▶ Single shared lockfile for entire monorepo
├── packages/
│   └── common-auth/          ──▶ Shared internal library
│       └── pyproject.toml
└── services/
    ├── api-server/           ──▶ Depends on "common-auth" via path
    │   └── pyproject.toml
    └── worker-engine/        ──▶ Depends on "common-auth"
        └── pyproject.toml

Benefits: - Single Lockfile: All services and packages share a single deterministic dependency resolution graph. - Instant Symlinking: Changes to common-auth are immediately reflected in api-server without publishing or reinstalling packages. - Fast CI: Re-uses cached pre-compiled wheels across all subprojects.


Pitfalls

Pitfall 1: Breaking OS Packages with sudo pip install (PEP 668)

Modern Linux distributions enforce PEP 668 (EXTERNALLY-MANAGED). Attempting to install packages into the global system Python with sudo pip risks breaking system tools (like apt, yum, or systemdaemons):

error: externally-managed-environment
× This environment is externally managed.
To install Python packages, create a virtual environment with 'uv venv'.

Always use project virtual environments (uv venv or uv run) instead of global system-level modifications.

Pitfall 2: Committing .venv/ into Version Control

Virtual environments contain platform-specific binaries, architecture-dependent C-extensions, and local absolute filesystem symlinks. They are not portable across machines:

# ALWAYS add to .gitignore:
.venv/
__pycache__/
*.pyc

Version control should only store pyproject.toml (human-readable dependencies) and uv.lock (reproducible binary lockfile).


Exercises

  1. Create a script that checks sys.prefix == sys.base_prefix and exits with an error code if it is not running inside a virtual environment.
  2. Write a single-file script using PEP 723 inline metadata that specifies a dependency on httpx and fetches a test URL using uv run.
  3. Create a minimal pyproject.toml project file with uv init my-project and inspect the generated layout and configuration keys.
  4. Inspect the contents of .venv/pyvenv.cfg and identify the base Python interpreter home path.

Further reading

  • PEP 621: Storing project metadata in pyproject.toml.
  • PEP 668: Marking Python base environments as “externally managed”.
  • PEP 723: Inline script metadata.
  • Astrid toolchain: Modern packaging with uv.