Workspaces, Virtual Environments, and Dependency Isolation
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.pyuv 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
- Create a script that checks
sys.prefix == sys.base_prefixand exits with an error code if it is not running inside a virtual environment. - Write a single-file script using PEP 723 inline metadata that specifies a dependency on
httpxand fetches a test URL usinguv run. - Create a minimal
pyproject.tomlproject file withuv init my-projectand inspect the generated layout and configuration keys. - Inspect the contents of
.venv/pyvenv.cfgand 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.