What does `python -m build` produce in dist/, and how is the wheel built?
answer
- A frontend that calls somebody else's hooks
- Two files land in one directory
- One artefact is built out of the other
- That ordering is a completeness check
- Nothing ever empties dist/ for you
basics
~20 sIt writes two artefacts into dist/: a source tarball and a wheel. By default it builds the sdist first, unpacks it, and builds the wheel from that unpacked copy rather than from your working tree.
solid answer
~50 s`python -m build` is a build *frontend*. It reads the build-system table from `pyproject.toml`, creates an isolated environment, installs the build requirements declared there, and calls the backend's hooks. Run with no arguments it does two builds: first the sdist, then it unpacks that tarball into a temporary directory and builds the wheel **from the unpacked sdist**. That ordering is a deliberate self-check — if a file is missing from the sdist, the wheel build fails there rather than shipping a broken tarball to users. Both files land in `dist/`, named `name-version.tar.gz` and `name-version-<tags>.whl`. `--sdist` and `--wheel` build only one artefact, and `--wheel` on its own builds straight from the working tree, skipping the check. `--outdir` moves the output. A stale `dist/` is the classic trap: it accumulates previous versions, and a glob upload will happily push them.
code
python · 5 linesproject_name = "Geo-Batch"
version = "1.4.0"
normalised = project_name.lower().replace("-", "_").replace(".", "_")
print(f"dist/{normalised}-{version}.tar.gz")
print(f"dist/{normalised}-{version}-py3-none-any.whl")go deeper
Know that the command writes a .tar.gz and a .whl into dist/, and that those two files are what gets published — you are not expected to explain the hook protocol yet.
Explain the frontend/backend split, the default sdist-then-wheel-from-sdist order and why that order exists, and what --sdist, --wheel and --outdir change about a run.
Demonstrate release hygiene: clean output directories, artefacts inspected before upload, uploads that name exact files, and a build matrix when the project is not pure Python.
Own the release pipeline as a supply-chain surface — where builds run, whether isolation is enforced, how artefacts are attested and how a bad release is contained given that published versions are immutable.
### Frontend and backend Building a Python distribution is split in two. The **backend** is the library that actually knows how to turn your source tree into artefacts; the **frontend** is the tool you run, which provisions the backend and calls its hooks. `python -m build` is a frontend, and the interesting thing about it is how little it does itself: it reads the build-system table in `pyproject.toml` to learn which backend to import and which packages that backend needs, prepares an environment containing them, and then invokes two hooks — one that produces a source distribution, one that produces a wheel. Installers do the same thing on the source-install path, which is why building locally with a frontend is a faithful rehearsal of what a user's install will do. ### The default is two artefacts, in order Run with no arguments, the frontend performs a sequence that surprises people the first time they watch the log: 1. Build the sdist from the working tree. Output: `dist/name-version.tar.gz`. 2. Unpack that tarball into a temporary directory. 3. Build the wheel **from the unpacked copy**, not from the working tree. Output: `dist/name-version-<tags>.whl`. Step 3 is the payoff. Your working tree contains everything — files under version control, untracked scratch files, generated assets, the whole history of your editing. The sdist contains only what the backend was told to include. If those two sets differ in a way that matters, building the wheel from the sdist fails *now*, on your machine, instead of failing later for the one user whose platform had no wheel and who therefore took the source path. Building from a clean unpacked copy is the cheapest sdist-completeness test there is. ### The flags that matter `--sdist` and `--wheel` restrict the run to one artefact. It is worth knowing that `--wheel` alone changes semantics, not just scope: with no sdist to unpack, the wheel is built directly from the working tree, so the completeness check is skipped. That is the right choice inside a fast iteration loop and the wrong one for a release build. `--outdir` (`-o`) redirects output somewhere other than `dist/`. `--no-isolation` reuses the current environment instead of provisioning a fresh one, which is what you want on an offline or air-gapped builder where the build requirements are already installed and there is no index to fetch them from; it costs you the guarantee that the declared build requirements are actually complete. ### What ends up in each file The sdist is a gzipped tarball whose single top-level directory is `name-version`, containing the source the backend chose to ship plus a generated `PKG-INFO`. Filenames use the normalised project name, so a project written with hyphens or mixed case in its metadata appears lowercased with underscores on disk — which is why the tarball's name does not always match the name you typed in `pyproject.toml`. The wheel's name carries the compatibility tags decided by the backend: a pure-Python project gets `py3-none-any`, and a project with compiled code gets tags naming the interpreter, ABI and platform of the machine that built it. That last point is worth internalising: **a compiled wheel is valid for the environment that produced it**, so a release covering several platforms requires several build machines or a cross-build matrix, not several invocations on your laptop. ### The stale-dist trap `dist/` is an ordinary directory and nothing empties it. After a few iterations it holds several versions, and an upload step written as a glob over `dist/` will offer all of them to the index. On a good day the index rejects the ones already published; on a bad day you publish a build you never intended to release, and since released versions are immutable you cannot quietly take it back. Delete `dist/` at the start of a release build, or build into a fresh directory with `--outdir`, and have your release job upload the exact filenames it just produced. ### Why build locally at all Because the artefacts are the deliverable and you should look at them before anyone else does. Both formats are ordinary archives: the stdlib's `tarfile` and `zipfile` modules will list the contents in a few lines, and that listing answers the questions that matter — did the data files make it in, is the test suite where I expected, did I accidentally ship a local configuration file with a credential in it. A release you have not listed is a release you are guessing about.
- Why is building the wheel from the sdist rather than the working tree the safer default?Because the working tree contains files the sdist may not. Building from a clean unpacked tarball proves the sdist is self-sufficient, so anyone who has to take the source path — a new interpreter, an unbuilt platform, a distribution packager — gets something that actually builds. Skipping that step is how projects ship a tarball missing a header, a template or a version file and only learn months later.
- When would you pass --no-isolation to a build run?On a builder that has no index access, or where the build requirements are already pinned and installed and you want to build exactly those. The tradeoff is that isolation is what proves your declared build requirements are complete; without it a build can silently succeed on a dependency you never declared, and fail on a clean machine.
- Your release job uploads dist/* and pushed an old version by accident. How do you prevent a repeat?Build into a clean directory every time — remove `dist/` first or use `--outdir` on a fresh path — and have the upload step reference the exact filenames the build just emitted rather than a glob. On the index side the damage is limited: published versions are immutable, so the fix is to yank the release rather than replace it.
saying these in an interview costs you the question
- Thinks the frontend itself compiles the code
- Believes the wheel is always built from the working tree
- Cannot name the two artefacts a default run produces
- Assumes one local run yields wheels for every platform
- Never inspects dist/ before uploading