skip to content

Why does a file listed in MANIFEST.in appear in the sdist but not the wheel?

level: middleimportance: should knowfreq 35%

answer

  1. Two archives are built, not one
  2. One file describes only the source tree
  3. A setting bridges the two
  4. Inside a package, or nowhere
  5. include matches one level; recursive-include descends

basics

~20 s

MANIFEST.in tells setuptools which extra files to add to the source distribution. The wheel is built from package-data rules instead, so unless include-package-data is enabled and the file sits inside a package directory, the wheel leaves it out.

solid answer

~50 s

You are building two archives with two different inclusion rules. `MANIFEST.in` is a setuptools file describing the **source distribution**: its `include`, `recursive-include`, `graft`, `exclude` and `prune` directives select files relative to the project root, and they routinely pull in things a wheel should never carry, such as tests, licences and CI configuration. The wheel is assembled from the packages plus their declared package data. The bridge between them is `include-package-data`: when it is true -- the default for pyproject-configured setuptools -- files that `MANIFEST.in` captured **and** that live inside a package directory are also copied into the wheel. So a file fails to reach the wheel for one of two reasons: it is outside any package, or the setting is off and no `[tool.setuptools.package-data]` glob names it. Diagnose by listing both archives, never by reading the config and reasoning.

code

python · 4 lines
python
from importlib.resources import files

package = files("email")
print(sorted(e.name for e in package.iterdir() if not e.name.startswith("__")))

go deeper

for a junior

Know that a build produces two archives -- a source distribution and a wheel -- and that they can hold different files. If a resource is missing after install, list both archives before changing any configuration.

for a middle

Explain the split cleanly: MANIFEST.in selects files for the sdist, package-data rules select for the wheel, and include-package-data bridges them for files that sit inside a package. Know that include is one level and recursive-include descends.

for a senior

Show how you would find this in a live incident -- a clean rebuild, a listing of both archives, and a wheel-install smoke test added to CI so the same omission cannot recur silently. Explain why an editable install hid it.

for a principal

Set the policy: which backend the organisation standardises on, whether tests ship at all, and a shared release pipeline that verifies artefact contents so each team is not re-deriving MANIFEST semantics from a stack-overflow answer.

### Two archives, two rule sets `python -m build` produces a source distribution and a wheel, and it is tempting to think of the wheel as a compiled version of the sdist. They are assembled by different code paths with different questions in mind. The **sdist** answers: *what does someone need in order to rebuild this project from scratch?* That is a superset of the runtime files -- test suites, tool config files, licence texts, a `Makefile`, header files for a compiled extension. In setuptools, `MANIFEST.in` is the language for that question. Its directives operate on paths relative to the project root: `include LICENSE`, `recursive-include src/converter/templates *.html`, `graft tests`, `prune .github`, `global-exclude *.pyc`. The **wheel** answers a much narrower question: *what has to be installed for the package to work?* Its content is the set of importable packages and modules, plus whatever was declared as their package data, plus the generated metadata directory. Test suites and CI files are not supposed to be there, and by default they are not. ### The bridge, and its two failure modes Setuptools connects the two with `include-package-data`. When it is true -- the default for a project configured through `pyproject.toml`, and false for a legacy `setup.py` build -- setuptools takes the files the sdist logic gathered and, for those that fall **inside a package directory**, copies them into the wheel too. That gives the two common failures: 1. **The file is outside every package.** A top-level `templates/` directory next to `pyproject.toml` is happily grafted into the sdist and can never become package data, because there is no package to attach it to. The fix is to move it under the package, not to add another directive. 2. **The bridge is switched off, and nothing else names the file.** In a legacy build, or where `include-package-data = false` was set deliberately, only an explicit `[tool.setuptools.package-data]` glob puts a file in the wheel. `MANIFEST.in` alone will not. There is a third, sneakier variant: the directive itself matches less than you think. ### The off-by-one that reaches production A document-conversion queue renders HTML previews from templates shipped inside the package. `MANIFEST.in` says: ``` include src/converter/templates/*.html ``` That directive matches exactly one directory level. `templates/invoice.html` ships; `templates/partials/header.html`, one level deeper, does not. Every test passes, because tests run against the source checkout where the file is simply present on disk. The sdist looks fine to a casual glance. The wheel is missing one subdirectory, and the failure surfaces only for the document types whose template happens to include a partial -- an intermittent-looking error that is in fact perfectly deterministic per document type. The fix is `recursive-include src/converter/templates *.html` or `graft src/converter/templates`, and the lesson is that `include` takes a glob, not a subtree. ### How to diagnose it in two minutes Stop reading configuration and inspect artefacts: ```console python -m build tar -tzf dist/converter-1.0.0.tar.gz | grep templates python -m zipfile --list dist/converter-1.0.0-py3-none-any.whl | grep templates ``` The difference between those two listings *is* the answer. If the file is in neither, the `MANIFEST.in` directive is wrong. If it is in the sdist only, you are looking at one of the two bridge failures above. A stale `build/` directory or a cached `.egg-info` can also serve old contents, so a clean build is part of the ritual. ### Why editable installs hide all of this An editable install points the interpreter back at your working tree, so every file on disk is reachable regardless of packaging rules. Local development and a source-tree test run therefore cannot detect a packaging omission by construction. The only test that can is one that installs the built wheel into a clean environment and exercises the resource. ### Backends without a MANIFEST `MANIFEST.in` is setuptools' own format. Hatchling and flit do not use it: they select files from the package directory, honouring version-control ignore rules, and are configured with include and exclude lists in their own tables. Migrating a project between backends means translating those rules by hand -- and re-running the artefact listing afterwards, because the defaults do not agree.

  • Your MANIFEST.in directive matches the top-level templates but misses a nested subdirectory. What is wrong?
    `include` takes a glob evaluated at a single directory level, so a pattern ending in `templates/*.html` never descends into `templates/partials/`. Use `recursive-include templates *.html` to walk the subtree, or `graft templates` to take the directory whole and then subtract with `global-exclude`. Confirm with a listing of the built archives rather than by re-reading the pattern.
  • Why do your tests pass while the wheel is missing a resource?
    Tests usually run against the source checkout or an editable install, where every file is present on disk regardless of packaging rules, so the packaging layer is never exercised. Add one CI job that builds the wheel, installs it into a clean environment, and imports the package to read its resources -- that job is the only one that can fail for this reason.
  • Should test files ship in the sdist, the wheel, or neither?
    The sdist is the reasonable home: it lets a downstream packager rebuild and verify the project from source. The wheel should carry runtime code and resources only, because tests inflate the install and can collide in the top-level namespace when they are not inside the package. Neither is defensible too, if you accept that consumers rebuild from your repository rather than from the sdist.

saying these in an interview costs you the question

  • Thinks MANIFEST.in controls the wheel directly
  • Adds more directives instead of moving the file into the package
  • Cannot say which archive a directive affects
  • Debugs by re-reading config rather than listing the built artefacts
  • Assumes include descends into subdirectories
  • Concludes packaging is fine because an editable install works

context