Diagnose pytest Discovery and src-Layout Import Errors
Separate zero collected tests from an import failure, install into the correct environment, and prove the selected test catches a broken assertion.
“No tests collected” and an import error are different failures. Changing an assertion will not fix discovery, and renaming a file will not install your package. This lab separates those stages in a small src-layout project, then proves the selected test can fail.
1. Use one interpreter from the project root
These PowerShell commands were checked with Python 3.12.14 and pytest 9.1.1. Start in a new empty practice directory. The broader introduction to assertions and test design is in the course lesson.
python -m venv .venv
$py = (Resolve-Path .venv/Scripts/python.exe).Path
& $py -m pip install pytest==9.1.1
& $py -m pytest --version
& $py -c "import sys; print(sys.executable)"
On macOS/Linux the environment interpreter is .venv/bin/python; use that same interpreter for pip and pytest. Activation is optional when you call the interpreter directly. Run every following command from this project's root.
2. Build a src-layout fixture
Create pyproject.toml:
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "xt-discovery-lab"
version = "0.0.1"
[tool.setuptools.packages.find]
where = ["src"]
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = ["--import-mode=importlib"]
Create an empty src/discovery_lab/__init__.py, then save src/discovery_lab/totals.py:
def total_cents(prices):
return sum(prices)
Create tests/checks_totals.py with this deliberately undiscoverable function:
def check_total():
assert 125 + 275 == 400& $py -m pytest --collect-only -q
$LASTEXITCODE
Expect no tests collected and exit code 5. With the configuration above, normal discovery looks under tests for test_*.py or *_test.py files, and for test_ functions. Naming both the file and function matters.
3. Fix naming before investigating imports
Rename-Item tests/checks_totals.py test_totals.py
& $py -m pytest --collect-only -q
The file name now matches, but the function still does not, so zero tests remain the expected result. Now replace the file with:
from discovery_lab.totals import total_cents
def test_total():
assert total_cents([125, 275]) == 400
Run collection again. In this fresh environment, it stops with ModuleNotFoundError: No module named 'discovery_lab' and exit code 2. pytest found and imported the test module, but the src-layout application package is not installed. This particular run is a collection failure; exit code 2 can also represent other interruptions.
4. Install the application into that environment
& $py -m pip install -e .
& $py -c "import discovery_lab; print(discovery_lab.__file__)"
& $py -m pytest --collect-only -q
The package path should point inside this practice project's src/discovery_lab. Collection should print:
tests/test_totals.py::test_total
1 test collected
The duration and path separators vary. An editable install connects this environment to the source package; it does not install the project into every Python interpreter. --import-mode=importlib avoids pytest adding test directories to the import path. Do not add random sys.path edits to hide a missing installation.
5. Run the collected node and prove failure detection
& $py -m pytest -q tests/test_totals.py::test_total
$LASTEXITCODE
Expect one passed test and exit code 0. Temporarily change the expected result to 401. Repeat the same command: it must report assert 400 == 401, one failed test and exit code 1. Restore 400 and run & $py -m pytest -q to confirm the whole practice suite passes.
| Observed result | Next check |
|---|---|
| No tests collected; code 5 | Check root directory, testpaths, file names and function names. |
| Collection import error; code 2 in this lab | Check the traceback, environment interpreter and editable package installation. |
| One collected test fails; code 1 | Read the assertion values and application behavior. |
| One test passes; code 0 | Confirm its node ID and deliberately break its assertion once. |
6. Diagnose an existing project without changing its rules
First run collection with the project's normal configuration and inspect the printed root directory. Custom python_files, python_functions, ignore rules and plugins can change discovery. An explicit file or node ID helps isolate a target, but does not prove that a normal full-suite run discovers it. If another interpreter imports a different copy, print its executable and the application's __file__ there too.
Exercise: rename the passing function to check_total again. Normal collection returns to zero tests. Restore test_total and the passing run. Keep this distinction in a bug report: discovery, collection/import, execution, or assertion.
References: pytest discovery and src layouts and pytest exit codes.
About
Separate zero collected tests from an import failure, install into the correct environment, and prove the selected test catches a broken assertion.
- Responsible
- Jay P
- Last Update
- 09/21/2026
- Reading Time
- 4 min
Reviews
Sign in to write a reviewComments (0)
Sign in to commentNo comments yet.