What a single `pip install` actually does
Type pip install requests, hit enter, and logs scroll for a few seconds.
In that window pip finds files in an index, decides which versions to install,
builds from source if it has to, and finally places files on disk. This walks
that usually invisible process one step at a time.
The pip docs frame this as four phases: identify requirements, resolve dependencies, build wheels, install. The sections below follow that order.
What you download: wheels and sdists
The files pip pulls from an index come in two main kinds: wheels and sdists.
An sdist (source distribution) is a tar archive holding source code. It has not been built, so installing from it needs a build step first. A wheel is a zip archive that is already built. Inside are only the files to install and their metadata, so installing is just moving them into place. That means smaller downloads and faster installs.
When both are on PyPI, pip prefers a wheel compatible with your platform. If a wheel exists, pip skips the build entirely. It downloads the sdist and builds only when no matching wheel is available.
Even a pure-Python package should ship both an sdist and a wheel. The sdist is a fallback for platforms without a matching wheel, and an archival snapshot of the source.
Choosing versions: the backtracking resolver
Before pip can pick files, it has to decide which versions to install. That is dependency resolution. Since version 20.3, pip uses a resolver that can backtrack.1
It works like this. pip assumes a version to install, then checks whether that
assumption holds. When it finds an assumption is wrong, it backtracks: it
discards some of the choices made so far and tries a different path. The
algorithm sits on top of the resolvelib library and prunes the search space
with constraint propagation to narrow the candidates.
If you have seen the same package downloaded at several versions during an install, that is backtracking. pip tries candidate versions one by one and downloads each. It is not a bug. This is how dependency resolution for Python packages works.
From sdist to wheel: PEP 517 and build isolation
If what you got is an sdist, it needs a build. How that build runs is set by two standards: PEP 518 and PEP 517.
PEP 518 introduced the pyproject.toml file and its [build-system] table.2 The
requires key lists the tools needed to build. Because the file is static TOML,
pip can read what to install first without running any project code.
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
PEP 517 added a build-backend key to the same table and defined the functions
a backend must expose.3 Building splits into two roles: the frontend a developer
invokes (a tool like pip) and the backend that produces the artifacts (a library
like hatchling or flit_core). Because the contract is standardized, any frontend
can drive any backend.
The build happens in an isolated environment. pip installs only the build
dependencies into a temporary directory and adds it to sys.path for the build.
This keeps build-only tools out of your runtime environment, and they disappear
once the build is done.
Unpacking the wheel: dist-info and console scripts
Now you have a wheel in hand. Installing it is essentially unzipping it into the right places.
A wheel holds the package directory with the actual code, and a
name-version.dist-info/ directory with metadata. pip moves this tree under
site-packages.
The dist-info directory has at least three files: METADATA with package
info, WHEEL with the wheel format version, and RECORD. RECORD lists almost
every installed file with its hash. Every file except itself is recorded with a
hash, so later integrity checks and uninstalls read straight from this list.4
A package that ships a command-line tool declares console_scripts in the
entry_points.txt inside dist-info. Each entry points at a function to run,
written as module:function.
[console_scripts]
mycli = mypkg.cli:main
An installer that understands this creates a command-line wrapper for each
entry. That is why you can type mycli in the shell right after installing. The
script body is not stored verbatim in the wheel. Only a string pointing at the
function is, and the wrapper is generated at install time.
Same result twice: cache, hashes, reproducibility
Run the same command twice and the second run is much faster. pip caches the wheels it builds and the files it downloads, so the next install skips the network and the build and pulls from the cache.5
But fast is not the same as reproducible. pip install requests can install
different versions today and tomorrow, if a new release lands in between. For a
reproducible install you have to pin versions. pip freeze produces a
requirements file that pins not just the top-level packages but all of their
transitive dependencies too.
Go one step further and you get hashes. Add --hash to each line of a
requirements file, and pip checks that the downloaded file matches the recorded
hash. It guards against remote tampering and corruption in transit.
requests==2.32.3 \
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
Wrapping up
A single pip install is really a small pipeline. It chooses versions
(resolve), decides what to fetch (wheel or sdist), builds in an isolated
environment when needed (PEP 517), and finally unpacks a zip into site-packages
(install). The logs just scroll past too fast to see it. Every install runs
these four phases in order.
Knowing the shape pays off in practice. If installs are slow and the same
package is fetched repeatedly, the resolver is backtracking. If it breaks during
a build, an sdist failed to compile. If a command is not found, the
console_scripts wrapper was not created. Each line of the log starts to map to
a phase.