skip to content

Why would pip suddenly build a dependency from its sdist when it previously installed a wheel in seconds?

level: seniorimportance: should knowfreq 45%

answer

  1. The install got slow but still succeeded
  2. The environment changed, not the package
  3. Tags are matched against the running interpreter
  4. Warm laptop, cold container
  5. Make the fallback fail instead of proceed

basics

~20 s

Because no published wheel matched the environment's tags any more, so pip fell back to the source tarball. A new interpreter, a new base image or architecture, or a release that shipped no wheel for that platform all cause it.

solid answer

~50 s

pip only installs a wheel whose tags match the running environment. When the match disappears — the image moved to a new CPython minor, the runner changed architecture, the base image swapped its C library, or the newly resolved version simply published no wheel for that target — pip falls back to the sdist and builds. The log says so explicitly: a `Building wheel for X` line means a source build, and `-v` shows which candidate files were considered and rejected. The first build is slow; the wheel it produces is stored in pip's cache and reused, so a warm machine looks fine and a fresh container is slow every single time. The fix is to stop it being silent: install with `--only-binary=:all:` (or the equivalent environment setting) for dependencies you expect to be pre-built, so a tag miss fails the job instead of quietly producing an environment built with a different toolchain from production's.

code

python · 6 lines
python
import sys
import sysconfig

print(f"{sys.implementation.name} {sys.version_info.major}.{sys.version_info.minor}")
print(sysconfig.get_platform())
print("free-threaded:", bool(sysconfig.get_config_var("Py_GIL_DISABLED")))

go deeper

for a junior

Recognise the Building wheel for ... line in an install log as the sign that pip is compiling from source rather than downloading a ready-made wheel, and know that this needs a toolchain.

for a middle

Explain the tag match: which properties of the running interpreter and platform decide it, what happens when nothing matches, and why the wheel cache makes the second install fast.

for a senior

Diagnose it end to end — read the verbose log, compare the environment before and after, check the cache, and then remove the ambiguity by enforcing binary-only installs or building a wheelhouse deliberately.

for a principal

Decide organisational policy: whether production environments may ever be assembled by a compiler at deploy time, how build provenance is recorded, and what a partially rolled-back deployment is allowed to be running.

### The mechanism pip computes the ordered list of compatibility tags the current environment supports — derived from the interpreter implementation and version, the ABI it was built with, and the platform. For each candidate release it ranks the available wheels against that list and installs the best match. If no wheel matches, and an sdist exists, pip downloads the sdist and runs the source-install path: provision the declared build requirements, call the backend, build a wheel, install it. Nothing warns you; the fallback is by design, and the only signal is that the log now contains a `Building wheel for ...` line and the step takes minutes. ### What actually changed Four causes cover nearly every real case. **The interpreter moved.** A compiled wheel is bound to a CPython minor version by its ABI tag, so an image that upgraded from one minor release to the next loses its match until the project publishes wheels for the new one — the reason "nothing installs on day one of a new Python release" is a recurring seasonal complaint. **The platform moved.** A runner switched from x86-64 to arm64, or the base image changed to one built against a different C library. Both are one-line image changes with no visible relationship to Python. **The version moved.** An unpinned dependency resolved to a newer release whose wheel matrix is narrower, or to a release published in a hurry as source only. **Configuration moved.** Someone set a source-only option — globally, in a config file, or via an environment setting — or a constraint disabled binaries for one package and, depending on how it was written, for its dependencies too. ### Diagnosing it Read the install log first: the presence of a build line names the package, and verbose output lists the candidate files pip looked at, which tells you whether wheels exist at all for that release or merely none that match you. Then compare the environment: the interpreter's version and ABI, and the platform string the running interpreter reports, are the two inputs to the tag computation, and both are available from the standard library without installing anything. Finally check the cache: pip stores wheels it built from sdists as well as ones it downloaded, so a developer laptop that built once is fast forever while a fresh CI container repeats the build on every run. That asymmetry is why this class of problem is so often reported as "only broken in CI". ### Why silence is the real defect Consider a four-person team running a nightly geocoding batch. A base-image bump quietly moves one compiled dependency onto the source path. The job still works, so nobody looks; the batch build now takes six minutes longer and the library is compiled against whatever the build image happened to provide rather than the tested binaries. Weeks later a release goes out, half the workers pick up the new environment before a failure trips a rollback, and the rolled-back half are running an artefact nobody ever built deliberately. Two builds of the same version and the same package are not the same artefact, and partial rollbacks are exactly where that bites. The cure is to make the fallback loud. Install with binary-only enforcement for the dependencies you expect to be pre-built, so a tag miss fails the job with a clear message at install time. Where a source build is genuinely required, make it explicit and deliberate: build the wheels once into a local wheelhouse with a dedicated wheel-building step, publish that directory as an artefact, and have deployment installs consume it with no index access at all. Both patterns turn an invisible behaviour change into a visible one. ### Cache management, precisely pip's cache has two layers: an HTTP layer for downloaded files and a local wheel store for wheels built from sdists. The `pip cache` subcommands report where it lives, list what is in it, remove selected entries and purge it entirely. In container builds the cache is usually the wrong optimisation — it inflates the image if you keep it and does nothing across builds if you do not — so the common practice is to disable it for the image build and rely on a wheelhouse or a pre-built base layer instead. Locally, the opposite holds: a stale cached wheel built against headers you have since changed is a genuinely confusing failure, and purging is the first thing to try when a rebuilt local dependency refuses to reflect your changes. ### The judgement to show The interview answer that lands is not "pip fell back to source". It is: I can tell a wheel install from a source build in the log, I know the three environment inputs that decide it, I know the cache makes the symptom intermittent between laptop and CI, and I make the fallback fail loudly rather than let a production environment be assembled by a compiler that nobody chose.

  • How do you make an unexpected source build fail the pipeline instead of silently succeeding?
    Install with binary-only enforcement — `--only-binary=:all:`, or the equivalent configuration set once for the job — so a tag miss raises an error naming the package instead of starting a compile. Where a source build is intended, do it in a separate, explicit wheel-building step whose output is a wheelhouse directory, and have the deployment install consume that directory with no fallback available.
  • Why is this problem so often described as only happening in CI?
    Because pip caches the wheels it builds from sdists. A developer machine pays the build cost once and is fast forever afterwards, while a fresh container starts with an empty cache and rebuilds every run. The cache hides the change locally, which is exactly why the log — not the wall-clock feel of the install — is the thing to check.
  • A locally rebuilt dependency keeps installing without your changes. What would you check first?
    The wheel cache. pip will happily reuse a wheel it built earlier for the same source, so your edits never reach the installed copy. Locate the cache directory, remove that package's entry or purge the cache, and reinstall; if you are iterating on the dependency itself, install it in editable mode instead so no cached artefact sits between the source and the interpreter.

saying these in an interview costs you the question

  • Blames the package rather than the environment tags
  • Thinks a slow install always means a slow network
  • Cannot tell a source build from a wheel install in the log
  • Assumes two builds of one version are the same artefact
  • Purges the cache as a fix without asking what changed
  • Leaves source fallback silently enabled in production installs

context