Under a src layout, how does automatic package discovery differ from a flat layout's?
answer
- Which directories become the wheel
- One root versus a whole repository
- Name-based exclusions at the top level
- Shipping tests as a top-level package
- Refusing to choose among candidates
basics
~20 sUnder a src layout the backend has one unambiguous root: everything under src/ is package content. A flat layout makes it guess among the repository's top-level directories, excluding tests and docs by name, and fail when several look equally plausible.
solid answer
~50 sA build backend has to decide which directories become part of the wheel. With a src layout that decision is trivial — `src/` holds the packages and nothing else, so discovery is exact and needs no configuration. With a flat layout the same algorithm has to sift the repository root, where the package sits beside `tests/`, `docs/`, `scripts/` and build output, and it falls back to a name-based exclusion list. Two failure modes follow: it silently ships something it should not, giving your users a top-level `tests` package in site-packages that can collide with another distribution's, or it finds several plausible top-level packages, refuses to guess, and fails the build until you configure discovery explicitly. The common setuptools backend behaves exactly this way; other backends usually infer `src/<name>` or `<name>` from the project name and hit the same ambiguity for the same reason.
code
python · 4 linesfrom importlib.metadata import distributions
for name in sorted((d.metadata["Name"] or "") for d in distributions()):
print(name)go deeper
Know that a build backend decides which directories go into the wheel, and that it can get this wrong. Be able to say that under a src layout the packages are the things under src/.
Contrast the two algorithms: an exact rule rooted at src/ versus a heuristic scan of the repository root with name-based exclusions, and name both flat-layout failure modes — shipping a stray top-level package, or failing the build on ambiguity.
Show you verify rather than trust: list the built wheel's contents, install it into a clean environment, and treat a missing module as a discovery defect. Explain why a stray top-level tests package harms users you will never meet.
Decide how the organisation guarantees this at scale — a project template that fixes the layout, a release check that inspects the built artefact, and a policy on when explicit discovery configuration is required rather than optional.
## What discovery has to decide When a build backend produces a wheel it must answer one question: *which directories in this repository are the package, and which are merely in the repository?* Nothing about the filesystem makes that obvious. A directory with an `__init__.py` looks like a package whether it holds your library or your test suite. Modern backends answer it automatically so that a minimal `pyproject.toml` — a name, a version, a build backend — produces a correct wheel with no file lists. That automation is convenient exactly to the degree that the layout makes the answer unambiguous. ## src layout: one root, no heuristics With a src layout the answer is structural. Everything under `src/` is package content; everything outside it is not. There is no list to maintain and no name to special-case, because the repository has told the backend where the boundary is by putting it in the directory tree. Add a subpackage, add a module, rename something — discovery keeps working, because the rule is "under src" rather than "matches these names". That is also why a src layout tends to need less packaging configuration than a flat one: the layout carries the information the configuration would otherwise have to state. ## flat layout: guessing at the root With a flat layout the backend must scan the repository root, where your package sits among everything else a repository contains. It uses heuristics: consider top-level directories that look like packages or single modules, and exclude the conventional non-package names — tests, docs, examples, scripts, tooling, build output, and similar. Two things then go wrong. **It includes something it should not.** A directory that the exclusion list does not know about is treated as part of your distribution. The classic case is a generically-named top-level `tests` package: it is built into the wheel, installed into site-packages as a top-level import name, and now collides with the `tests` package of any other distribution that made the same mistake. Whichever one is found first on `sys.path` wins, and the loser's test suite silently disappears — a genuinely confusing failure precisely because it appears in an unrelated project. **It refuses to guess.** When several top-level candidates survive the exclusion list, the common setuptools backend stops with an error about multiple top-level packages discovered in a flat layout rather than picking one, and asks you to configure discovery explicitly. This is the good outcome: a loud build failure beats a wrong wheel. But it is a build failure that a src layout would never have produced. ## The other direction: discovering too little The mirror-image failure is a module that discovery does not pick up at all, so the wheel is missing part of your library. The most common cause is a subdirectory that was never made a package — no `__init__.py`, so it does not look like package content. Under a flat layout you will not notice, because your own runs import the working copy where the file plainly exists; the wheel's omission only surfaces for someone who installs it. Under a src layout the first run after the install fails, because there is nothing at the root to fall back to. ## Checking what you actually built Discovery is worth verifying rather than trusting, and both checks are cheap. Build the distribution and list the wheel's contents — it is a zip archive, so the file listing is the definitive statement of what your users will get, and it is where you confirm that `src` was stripped and that no `tests` directory came along. Then install it into a clean environment and import it. From inside a process, `importlib.metadata.distributions()` enumerates what is installed by distribution name, which is a useful reminder that the installed name and the import name are different things and that discovery is what connects them. ```python from importlib.metadata import distributions for name in sorted((d.metadata["Name"] or "") for d in distributions()): print(name) ``` ## What this is not Discovery decides which *packages* go into the distribution. Which non-Python files ride along — templates, schemas, data tables — is a separate declaration with its own rules, and the fact that a file sits inside a discovered package does not by itself guarantee it is shipped. Keep the two ideas apart in an interview: a missing module is a discovery problem, a missing data file usually is not. The honest summary is that automatic discovery is a convenience built on an assumption about layout. A src layout satisfies that assumption by construction. A flat layout satisfies it by luck and by a list of directory names someone else chose.
- Why is a top-level tests package in a published wheel actually harmful?Because `tests` is installed into site-packages as a global import name. Any other distribution that shipped the same mistake now competes for it, and only the first one on `sys.path` is importable — so an unrelated project's test suite vanishes, with no obvious link to your release. It also bloats the install with files no user runs.
- Does src layout mean you never need to configure package discovery?Usually, but not by magic. The default rule is that packages live under `src/`, which matches almost every project. You still configure explicitly when the tree is unusual — several distributions built from one repository, a namespace package, or generated code assembled at build time. The point is that the layout removes the ambiguity, not the option.
saying these in an interview costs you the question
- Assuming the wheel simply contains the whole repository
- Thinking discovery is guaranteed to exclude tests
- Believing a directory without __init__.py is still discovered
- Confusing package discovery with declaring data files
- Treating a build error about ambiguity as a tooling bug