You run pip install dozens of times a week. You probably have no idea what actually happens when you do.
The machinery is surprisingly simple — and once you see it, every tool choice in the ecosystem starts making sense.
Wheels are zip files
Literally. Rename a .whl to .zip and unzip it.
cp requests-2.31.0-py3-none-any.whl requests.zip
unzip requests.zip -d requests_contents/
You’ll find the package source and a metadata directory. That’s the whole format. “Installing” a package is mostly extracting a zip into a specific folder. This is a big part of why uv is fast — there’s no compilation step for wheels, no complex transaction. It’s unzipping.
site-packages is just a folder
When you write import numpy, Python walks a list of directories — sys.path — looking for a match. The most important one is site-packages. That’s where your installed packages live.
python -c "import site; print(site.getsitepackages())"
The entire “install a package” operation is really just “put files in this folder.” Different tools do it differently:
- pip copies files into site-packages.
- uv hardlinks from a global cache — same file, two names. Saves disk, way faster.
- conda also hardlinks from its own cache.
That’s the whole mechanism. There’s no registry, no database. A folder.
A virtualenv is a PATH trick
A virtualenv is a directory with a bin/python symlink and its own site-packages. Activating it just prepends the venv’s bin/ to your PATH so that python resolves to the venv’s interpreter.
python -m venv .venv
source .venv/bin/activate
which python # .venv/bin/python
The “isolation” is that each venv has its own site-packages folder. Python sees that folder first, ignores the system one. That’s it — no containers, no namespaces, no magic. A symlink and a directory.
This is also why creating a venv is fast. There’s barely anything to create.
install vs sync — additive vs declarative
This distinction trips people up constantly.
pip install/uv pip install: “add these packages to the environment.” Anything already there stays.pip sync/uv pip sync: “make the environment look exactly like this lockfile.” Extras get removed.
Mental model: install is apt install. sync is terraform apply.
If you’ve ever wondered why your environment has 200 packages when your requirements file lists 12 — it’s because you’ve been using install for months and never cleaned up. sync is the fix.
The .dist-info folder matters
Next to every package in site-packages, there’s a metadata directory:
site-packages/
├── numpy/
├── numpy-1.26.4.dist-info/
│ ├── METADATA
│ ├── RECORD
│ ├── WHEEL
│ └── entry_points.txt
This is how tools know what’s installed. importlib.metadata.version("numpy") reads from here. pip list reads from here. Dependency resolution reads from here.
This is also why hacking site-packages — like Bazel’s rules_python sometimes does — breaks things. Packages expect .dist-info to exist in a specific location with specific contents. Fake it wrong and pkg_resources throws errors that make no sense until you know this.
Hardlinks vs symlinks vs copies
This comes up constantly in packaging tools, and most people hand-wave past it.
- Copy: duplicate the bytes. Slow. Doubles disk usage. Always works.
- Hardlink: two directory entries pointing to the same inode. No bytes copied — just a new name for the same file. Fast. But can’t cross filesystem boundaries.
- Symlink: a pointer to a path. Works across filesystems, but some tools don’t follow them correctly.
# hardlink — same inode
ln original.txt hardlink.txt
stat original.txt hardlink.txt # same inode number
# symlink — pointer
ln -s original.txt symlink.txt
uv uses hardlinks from a global cache by default. First install downloads and caches the wheel. Every subsequent install into any venv just creates hardlinks — no bytes copied, near-instant. This is probably the single biggest reason uv pip install feels unreasonably fast on a warm cache.
Python packaging feels broken because people don’t have a mental model of what’s happening. But the machinery is dead simple — zip files, a folder, a PATH change, and some metadata. Once that clicks, the tool choices — uv’s hardlinks, venvs over global installs, sync over install — stop being opinions and start being obvious.