Articles
Python Packaging Interview Questions: pyproject.toml, Build Isolation, and Imports
Prepare for Python packaging interviews with pyproject.toml, build isolation, wheel inspection, editable-install traps, and clean-environment import checks.
When a Python package works from its repository but fails after installation, start by testing the installed artifact outside the checkout. Identify the interpreter, distribution, imported module path, and missing file before changing dependency versions. The central interview distinction is between what builds a package, what it needs at runtime, and what your development directory accidentally supplies.
This guide targets Python backend and tooling interviews. Packaging rules come from official PyPA documentation; the small package and diagnostic experiments are original exercises. They are not claims about a particular employer's assessment.
What belongs in pyproject.toml?
pyproject.toml is project configuration, not a guarantee of a reproducible environment. Its sections describe different contracts. PyPA separates build configuration in [build-system], standardized project metadata in [project], and tool-specific settings under [tool]. A dependency declared for the build is not thereby a runtime dependency. PyPA configuration guide
For the exercise, create this small package with a src layout:
parcel-demo/
pyproject.toml
src/
parcel_demo/
__init__.py
cli.py
Use this deliberately minimal configuration:
[build-system]
requires = ["setuptools>=77.0.3"]
build-backend = "setuptools.build_meta"
[project]
name = "parcel-demo"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = []
[project.scripts]
parcel-demo = "parcel_demo.cli:main"
[tool.setuptools.packages.find]
where = ["src"]
Inside cli.py, define main() to print a simple message. Keep __init__.py empty initially. The distribution name is parcel-demo, while the import package is parcel_demo; installation and import names are related by packaging choices, not by a universal spelling rule. PyPA distribution and import packages
In an interview, justify every extra field you add. If the CLI imports a third-party HTTP client, put that runtime requirement in project dependencies. If tests use a test runner, keep that development concern separate. Do not add runtime libraries to build requirements simply because an import failed somewhere in the pipeline.
Why does build isolation uncover hidden dependencies?
Build isolation gives the build backend an environment separate from the caller's ordinary installed packages. Pip installs declared build requirements there and invokes the backend's hooks. Turning isolation off makes the caller responsible for supplying build dependencies; it does not repair an incomplete declaration. Pip build-system interface
Suppose version discovery imports parcel_demo, and that package imports an HTTP library at module scope. Your workstation has the library, so the build succeeds locally. The isolated environment lacks it, so metadata generation fails before any wheel is produced. Distinguish that failure from a consumer successfully installing the wheel and then failing at runtime.
A strong repair reduces the dependency of metadata generation on application startup. In our exercise, retain the static version while investigating; for dynamic versioning, use the chosen backend's documented mechanism. If a dependency really is needed to build, declare it as such. Avoid adding every application dependency merely to make an accidental import chain succeed.
Ask the interviewer which phase failed: installing build requirements, obtaining metadata, building the wheel, installing the artifact, importing the module, or executing the command. These are different observation points. A log ending in “could not build wheels” is a summary; find the first meaningful backend exception beneath it.
How do you prove the wheel contains the intended package?
A wheel is a built distribution intended for installation. A source distribution carries source material from which a build backend can produce a wheel. PyPA's packaging tutorial demonstrates producing both artifacts; testing only the checkout leaves their contents untested. Packaging Python projects
Here is an original Unix-style diagnostic sequence. Run it from the exercise root, using disposable directories. The explicit paths prevent the shell's activated environment from becoming an unstated assumption.
python3 -m venv .buildenv
.buildenv/bin/python -m pip install build
.buildenv/bin/python -m build
.buildenv/bin/python -m zipfile -l dist/parcel_demo-0.1.0-py3-none-any.whl
python3 -m venv /tmp/parcel-consumer
/tmp/parcel-consumer/bin/python -m pip install \
./dist/parcel_demo-0.1.0-py3-none-any.whl
cd /tmp
/tmp/parcel-consumer/bin/python -c \
'import parcel_demo; print(parcel_demo.__file__)'
/tmp/parcel-consumer/bin/parcel-demo
Use a fresh unique consumer directory if that example path already exists. In a Windows shell, adapt the environment executable paths instead of copying Unix paths blindly. The important properties are a clean consumer environment, an explicit artifact, and a working directory outside the project.
Expected evidence is a module path inside the consumer environment, followed by the CLI's message. A successful command launched from the checkout is weaker evidence because local source files can mask an incomplete installation. Inspect the wheel listing for parcel_demo/__init__.py and parcel_demo/cli.py, along with distribution metadata.
For a second failure, add templates/greeting.txt to the package and read it at runtime. Now inspect both source and wheel archives for that file. Configure package-data inclusion using the backend's documented controls. Do not assume “tracked by Git” means “present in the wheel.” Setuptools data-file support
Why can editable installs hide a broken release?
An editable installation supports development against source files. It does not establish that a regular wheel contains the same accessible files or behaves identically. Pip explicitly recommends testing regular installs for deployment and notes backend-dependent differences. Pip local-project installs
Imagine your tests read Path('templates/greeting.txt'). They pass from the repository root because the file happens to exist there. The installed command fails from /tmp because its current directory differs. Two separate questions follow: was the resource shipped, and is the code locating it independently of the working directory?
For a package resource, prefer a package-relative resource interface rather than guessing the checkout path:
from importlib.resources import files
text = (
files("parcel_demo")
.joinpath("templates/greeting.txt")
.read_text(encoding="utf-8")
)
This still requires the data file to be included in the distribution. Resource lookup cannot conjure a missing file. If a third-party API requires an actual filesystem path, investigate the resource API's context-managed extraction rather than assuming every installed resource is a persistent ordinary path. Python package-resource documentation
PyPA explains that src layout helps separate importable application code from repository-root files. It encourages testing through installation instead of accidentally importing from the current directory. That improves the experiment's design, but it cannot prove correct metadata or data inclusion by itself. PyPA layout comparison
What if pip says installed but import still fails?
First identify the executable rather than reinstalling everything:
python -c 'import sys; print(sys.executable)'
python -m pip --version
python -m pip show parcel-demo
python -c 'import parcel_demo; print(parcel_demo.__file__)'
These commands answer different questions. The first names the interpreter. The second locates pip under that interpreter. The third inspects installed distribution metadata. The fourth actually executes import resolution. “Installed” is therefore not equivalent to “the expected module was imported.”
In our exercise, rename a local helper file to the name of a library you intend to import. Run from that directory and inspect the imported module's path. This demonstrates shadowing: an unexpected local file can win import resolution. Repair the misleading filename and verify in a fresh process; do not treat repeated reinstalls as evidence that a search-path problem has disappeared.
If a console command fails but python -m execution works, compare the command's environment and entry-point target. Does the referenced callable exist in the installed artifact? Is the shell selecting an older executable earlier on its path? Keep diagnostics tied to the specific consumer environment rather than the machine's globally installed commands.
If a distribution name differs from its import package, consult that project's documentation. Guessing a new package from an import error is unsafe and can install an unrelated distribution. The naming distinction should change your investigation, not just become another definition to memorize.
How would you design a release verification matrix?
Use a matrix that isolates the source of success. The following checks are our recommended exercise design, not a claim that every project needs identical CI jobs.
| Environment | What to execute | Failure it can expose |
|---|---|---|
| Checkout with editable install | Fast unit tests | Development logic regressions |
| Fresh environment, installed wheel, outside checkout | Import and CLI smoke test | Missing modules, wrong entry point, path dependence |
| Wheel built from the source archive | Same consumer smoke test | Files omitted from the source distribution |
| Each supported interpreter or platform | Focused compatibility tests | Unsupported syntax or incompatible native artifacts |
Choose a test that exercises the shipped behavior, not just import parcel_demo. For the template example, assert the exact greeting produced by the installed command. That single assertion crosses package discovery, data inclusion, runtime resource lookup, and entry-point wiring.
Record the artifact filename and checksum alongside the interpreter version and command output. If another environment fails, this establishes whether you are discussing the same bytes. A version number alone may not distinguish locally rebuilt artifacts during debugging.
For native extensions, expand the experiment to platform and interpreter compatibility. A wheel containing compiled code has different portability constraints from the pure-Python exercise. Do not generalize py3-none-any from this example to every wheel. PyPA platform compatibility tags
Does build isolation make a build reproducible?
No. Isolation separates environments; reproducibility asks whether controlled inputs produce the intended repeatable result. Our example's lower-bound build requirement allows newer backend releases, so it is explicitly not a fully pinned build recipe.
Explain the remaining inputs: interpreter, backend version, resolved dependencies, platform tools, environment variables, generated files, and source revision. Decide whether the interview means repeatable installation behavior or byte-identical artifacts. Those are different targets and require different evidence.
For a library, dependency ranges describe compatibility for consumers. For a deployed application, a controlled resolved environment may be appropriate. Avoid claiming that one unqualified pinning rule solves both. Name the packaging tool and the lock or constraints behavior you actually use before promising exact reproduction.
The best final debugging statement is concrete: “The wheel omitted the template, while checkout tests read it from the working directory. I added backend data inclusion, switched to package-relative resource lookup, and verified the installed CLI from a clean directory.” That explains the defect and the proof without hiding behind tooling jargon.