한국어
Youngkwang Yang

A personal tech blog

Contents

What a single `pip install` actually does

Tooling

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.

yesnopip install Xresolve: choose versionsprebuilt wheelfor this platform?download wheeldownload sdistbuild wheel inisolated envinstall: unpackinto site-packages

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.

backtrackpick A==2.0A needs C<2pick B==3.0B needs C>=2:conflictdrop A==2.0,try A==1.9

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.

mypkg-1.0-py3-none-any.whl(a zip)mypkg/ (*.py)mypkg-1.0.dist-info/site-packages/mypkg/site-packages/mypkg-1.0.dist-info/entry_points.txtbin/mycli (wrapper)

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.

Further reading

Footnotes

  1. pip docs, Dependency Resolution

  2. PEP 518, Specifying Minimum Build System Requirements

  3. PEP 517, A build-system independent format for source trees

  4. Python Packaging User Guide, Binary distribution format

  5. pip docs, Caching