skip to content

Why does installing a Python sdist execute package code when a prebuilt wheel does not?

level: middleimportance: should knowfreq 48%

answer

  1. source archive versus already-built artifact
  2. the installer must build the sdist first
  3. a build backend is arbitrary code
  4. wheel install unpacks; nothing of the package runs
  5. the rest waits for an import

basics

~20 s

An sdist is source, so the installer has to build it first, and that build runs the package's own build code. A wheel is already built, so installing it only unpacks files and metadata. The package's code then waits for an import.

solid answer

~50 s

An sdist ships source, so before anything can be installed the installer must build a wheel from it, and that build invokes the package's build backend - arbitrary code running on the build machine as the installing user. A wheel is the finished artifact: installing it copies files and metadata into place and runs none of the package's own code. The distinction splits capability in two. **Install-time** code runs on whatever machine performed the install - often a build runner holding credentials the application itself never sees - and it runs unconditionally, once per package in the tree. **Import-time** code runs later, inside the application's process, and only for the modules something actually imports. Preferring wheels removes the install-time half. It does not make the package trustworthy, because a module body still executes the moment anything imports it.

go deeper

for a junior

Know that a Python package can arrive as source or as a built artifact, and that installing the source form requires a build step which runs code from the package.

for a middle

Explain the mechanism: an sdist install invokes the project's build backend, a wheel install only unpacks files and metadata, and byte-compilation is not module execution.

for a senior

Argue the capability difference - unconditional execution on a credential-bearing build host versus conditional execution in the application process - and say what a wheel-only policy does and does not buy.

for a principal

Be able to defend a source-build exception for teams that genuinely need local compilation, and place the residual trust deliberately: publisher binaries you cannot read, or build code you execute yourself.

## Two artifact formats, two behaviours A Python project can be published in two shapes. A **source distribution (sdist)** is essentially the project's source tree in an archive. A **wheel** is a built distribution: the files already laid out as they will land in the environment, plus metadata, plus any compiled binaries the publisher produced. When the installer is handed a wheel it does very little that is interesting - it validates the archive, copies files into the environment, writes metadata and entry-point launchers, and optionally byte-compiles `.py` files to `.pyc`. Byte-compilation reads and compiles the source; it does not execute the module body. So no code belonging to the package runs. When the installer is handed an sdist it cannot do any of that yet, because nothing has been built. It must invoke the project's **build backend** to produce a wheel first. That backend is code shipped by the package - historically a `setup.py`, today a backend named in the project's build configuration - and running it means executing the publisher's code on your machine, with your environment. Everything an install hook in another ecosystem could do, a build backend can do here. ## Install-time versus import-time capability This is the axis worth internalising, because it decides *where* and *under what conditions* an attacker's code runs. | | Install-time | Import-time | |---|---|---| | Runs on | the machine that installed - laptop, build runner, image build | the machine that runs the application | | Runs as | the installing identity, with the job's environment | the application process's identity | | Trigger | unconditional, every install | only if something imports the module | | Sees | build secrets, source checkout, caches, registry tokens | runtime configuration, application data, production network | Neither is harmless, but they are not interchangeable. Install-time execution is usually the more attractive position on a build machine: it is unconditional, it happens on a host that frequently holds publishing or deployment credentials the running application never has, and it happens before anything inspects the tree. Import-time execution is the more attractive position if the goal is production data, because that is where the application's own access lives. ## A concrete choice: the base image that builds from source A data-science base image often forces source builds so that numerical libraries compile against the exact hardware and toolchain of the target machine. That is a real engineering reason, and it is also a decision to execute every one of those packages' build code inside the image build - a build that commonly has a package index token, cloud credentials, and sometimes datasets mounted so that a step can validate against them. The alternative, installing publisher-built wheels, executes nothing during the image build but moves the trust to a binary the publisher compiled, which you cannot meaningfully read or diff. Neither option removes trust; they relocate it. What you can do is be explicit about which one you chose and why, and make sure the machine doing the source build is not also the machine holding the most valuable credential. ## The same axis in other ecosystems The pattern generalises. Some ecosystems execute nothing at install and hold their capability in a build tool's plugin model instead. Others execute code only for packages carrying a native extension, which must be compiled on the installing machine - a gem with a native extension compiling during an install on a developer's laptop is running arbitrary build code next to a live SSH agent and a logged-in cloud session, which is a different asset at risk from the CI case but the same mechanism. Others run a declared script for every package. The question to ask of any ecosystem is simply: on install, does anything from the package run, and if so, as whom. ## What preferring wheels actually buys It is a genuine reduction, and worth doing where you can: it removes an unconditional execution point on your build machines, and it makes builds faster and more repeatable as a side effect. It buys three things it is often mistaken for, though: - It does not make the package safe. Import-time code is untouched. - It does not verify the wheel. A wheel from a compromised publisher account is a wheel. - It does not mean nothing native runs. It means the native code was compiled somewhere else, by someone else. Saying that clearly is what separates a candidate who has read a hardening checklist from one who understands what the checklist changed.

  • If you force wheel-only installs, what execution have you left in place?
    Import-time execution. A module body runs the moment something imports it, in the application's own process with its configuration, credentials and network access. You have removed an unconditional execution point on build machines and kept a conditional one at runtime. That is a narrower attack surface, not a trusted dependency.
  • Does a wheel containing a compiled native extension execute code during installation?
    No. The compilation already happened on the publisher's machine; installation copies the binary into place, and it runs when the extension is loaded, which is an import-time event. The tradeoff is that you have moved from executing readable build source to trusting an opaque binary you cannot diff against the repository.
  • Why would a team deliberately choose source builds despite this?
    Because they need the artifact compiled against their own toolchain, hardware or linked libraries, or because no wheel exists for their platform. That is legitimate. The security answer is not to forbid it but to know it is a decision, and to keep the machine doing the source build away from credentials that matter.

saying these in an interview costs you the question

  • Says a wheel is safe because nothing runs at install
  • Thinks byte-compiling to .pyc executes the module body
  • Assumes import-time code is harmless compared to install-time
  • Believes wheels never contain compiled native binaries
  • Treats install-time and import-time execution as the same risk

context