datarekha
Infrastructure May 24, 2026

Where your Python packages actually live

venv isolates them, uv makes installing them almost free, and pex bundles them into one file you can throw at any machine. A field guide to three tools and the one question they each answer.

7 min read · by datarekha · pythonpackaginguvvenvpex

You run pip install pandas. It says “Successfully installed.” You feel good. Then you ask: installed where, exactly?

If you have been writing Python for more than a week, you have probably bumped into the answer at the worst possible time. The package installed fine on your laptop. The deploy fails. Your colleague’s script breaks after you install something unrelated. A job you wrote six months ago silently starts behaving differently because something upstream upgraded. The phrase “works on my machine” was practically coined for Python packaging.

The confusion is not accidental. Python’s default install target is a global site-packages directory shared by every script on the machine. Everything you pip install lands there together, which means every project shares the same dependency graph whether you want that or not. That is a workable setup for a single script and a slow-motion disaster for anything larger.

Three tools exist to fix three different parts of this problem. They are not competitors. They are answers to three distinct questions.

venv: where exactly does “isolated” mean?

A virtual environment is not magic. It is a folder.

Run python -m venv .venv and Python creates a directory with a specific structure: a bin/ folder containing a python symlink (not a copy of the interpreter — just a lightweight link back to the Python binary you called the command with), and a lib/pythonX.Y/site-packages/ directory that starts completely empty.

That is it. The isolation lives in that empty site-packages.

When you “activate” the environment with source .venv/bin/activate, your shell prepends .venv/bin to the PATH. The python that now answers when you type python is the one inside .venv/bin/. And because that binary has its own site-packages baked in as its home, any pip install you run after activation writes packages there — not into the global directory, not into any other project’s directory.

Open up .venv/lib/pythonX.Y/site-packages/ after installing something and you will see the package sitting right there. No abstraction, no container. A folder with wheel contents unpacked into it.

The practical consequence is large and simple: two projects can each have their own pandas at their own version, with no conflict, because each project looks in its own isolated directory. A requirements.txt or pyproject.toml pinned to specific versions means the environment is reproducible: delete the .venv folder, recreate it, run pip install -r requirements.txt, and you have the exact same graph back.

The one thing venv does not solve is speed. Creating an environment and installing fifty packages still takes meaningful time because pip downloads each wheel from PyPI every single time.

uv: why does it feel instant?

Astral’s uv is written in Rust, which is part of the story. But the bigger part is architectural, and understanding it changes how you think about the tool.

Every wheel uv ever downloads lives in a single global content-addressed cache at ~/.cache/uv. When you run uv pip install pandas in a fresh project, uv resolves the dependency graph, fetches any wheels it does not have yet, and unpacks them into the cache. The first install of a given wheel version takes as long as downloading it takes.

The second install — in any project on the same machine — takes almost no time at all. uv does not download the wheel again. It creates a hardlink from the cache into the new venv’s site-packages. On filesystems that support reflinks (like APFS on macOS and many modern Linux filesystems), it uses a reflink instead, which is even more efficient. Either way, the bytes live exactly once on disk. The “installation” is just a directory entry pointing at data that already exists.

This is the thing worth understanding about uv. It is not a faster download tool. It is a tool that largely eliminates the download from every install after the first. If you work across five projects that all use the same version of numpy, numpy exists once on disk and each project’s venv has a hardlink to it. Creating a fresh environment in CI, a Docker build that reuses the cache layer, or a colleague’s machine after they have already worked on similar projects — all of these become nearly instant.

~/.cache/uvpandas 2.2 • numpy 1.26 • …project-a/.venvsite-packages (links)project-b/.venvsite-packages (links)project-c/.venvsite-packages (links)hardlinkhardlinkhardlink
Each wheel is downloaded once into the global cache; every venv gets a hardlink — no extra disk bytes, near-zero install time.

uv also quietly absorbs jobs that used to require separate tools. uv venv replaces virtualenv. uv pip install replaces pip. uv add, uv lock, and uv sync give you project-level dependency management with a lockfile. uv python install can download and manage Python versions themselves, so you can stop thinking about pyenv too. The surface area is large, but the mental model is simple: one fast tool, one cache, everything else follows from that.

pex: the question nobody thinks to ask until deploy day

Both tools above assume the target machine will run pip install at some point. For most development workflows that is fine. For shipping code — particularly a CLI tool, an internal service, or a PySpark job running on a cluster you do not control — it becomes a liability.

The pex project (from pantsbuild) answers a different question: what if the code and all its dependencies could travel as a single file?

A .pex file is an executable zip. It bundles your code alongside the wheel archives for every dependency. You produce it once with something like pex my_package -e my_package.main:run -o app.pex, and the result is a single file you can scp to a server, drop into a Docker image, or hand to a Spark executor. On the target machine, running ./app.pex launches it directly.

The mechanism on the target is clean: when a .pex runs, it extracts its bundled wheels into a local cache (typically under ~/.pex) and adjusts sys.path to point at those extracted packages, bypassing the system’s site-packages entirely. The result is hermetic in the ways that matter most: no version conflicts with whatever else is installed on the host, no pip invocation, no network access at runtime.

This is especially valuable in data infrastructure. A PySpark job needs a specific version of a dozen libraries. The Spark workers may have an arbitrary Python environment — or a minimal one. Bundling the job as a .pex and passing it to the cluster means the job brings its own dependency universe. No coordinating with the cluster admin, no “this worked yesterday” mysteries. Internal CLI tools distributed to teammates who should not need to care about virtual environments also become much simpler: one file, one permission bit, one command.

Three tools, three questions

The mental model that makes all of this click is that each tool answers exactly one question:

venv answers: where do my packages live, and how do I keep them away from everything else?

uv answers: how do I install packages without waiting, and without managing five separate tools?

pex answers: how do I ship my code plus its dependencies to a machine I do not control, without running pip on it?

Using all three is not redundant. You develop in a uv-managed venv, which is fast and reproducible. You build a .pex for distribution, which is hermetic and portable. The venv gives you isolation during development; the .pex gives you isolation at runtime, somewhere else entirely.

Most packaging confusion comes from treating “where do packages live” as a single problem with a single answer. It is three problems. These are three answers.


The full mechanics — with diagrams showing what exactly is inside a .venv folder, how uv’s content addressing works, and how to build and inspect a .pex — are in the course notes under Python › Pythonic Mid-Level › Environments & Packaging.

Skip to content