Resource library

QA How-To

How to Fix pytest "collected 0 items"

Fix pytest collected 0 items by checking discovery paths, test names, filters, configuration, and CI or Docker setup, then verify tests actually run today.

17 min read | 3,499 words

TL;DR

Run python -m pytest --collect-only -q from the intended project root. If it lists no tests, check testpaths, filenames, function names, and ignore rules; if it lists tests, remove selectors from the failing command one at a time. Verify the corrected command executes at least one test.

Key Takeaways

  • Run python -m pytest --collect-only -q to see the exact tests selected before execution.
  • Check the current directory and active testpaths before changing the tests themselves.
  • Default recursive discovery matches test_*.py and *_test.py, then test-prefixed functions and Test-prefixed classes.
  • Compare unfiltered collection with -k, -m, --deselect, and node ID selections.
  • Inspect the actual CI checkout or Docker filesystem when only remote runs collect zero tests.
  • Keep pytest exit code 5 as a failing signal for jobs expected to execute tests.

To fix pytest collected 0 items, first check what pytest can discover with python -m pytest --collect-only -q. This message appears when collection finishes without a selected test, often because the runner started in the wrong directory, names do not match discovery rules, or a selection filter leaves zero tests to run. When a filter deselects discovered tests, pytest usually reports the collected and deselected counts rather than the literal collected 0 items line.

collected 0 items

Do not treat an empty run as a passing suite. Pytest returns exit code 5 when no tests are collected. Work through the checks below in order, change one cause at a time, and confirm that the collection list contains the test you intended to run.

TL;DR

Run these commands from the repository root:

pwd
python -m pytest --version
python -m pytest --collect-only -q
python -m pytest tests/ --collect-only -q

If the explicit tests/ path finds tests, inspect your working directory and testpaths. If both commands return zero, check file, function, and class names, then inspect ignore rules. If collection finds tests but your normal command runs none, remove -k, -m, --deselect, or a stale node ID and add each selector back separately. A collection-only run is the fastest safe diagnostic because it imports test modules and reports selected items without executing test bodies.

What the Error Actually Means

Pytest has a collection phase before execution. It chooses starting paths, visits eligible directories, imports matching Python files, creates test items, and then applies selection rules. collected 0 items says that the resulting item list is empty; it does not identify which phase removed the tests. The exact line can appear with no tests ran in the summary. Exit code 5 distinguishes this state from a successful run, while an import or syntax failure generally appears as a collection error with a different exit status.

The startup header gives two useful clues: rootdir and configfile. rootdir identifies the project root pytest selected for node IDs and cache placement. It does not automatically add your application package to sys.path, and it is not proof that pytest searched the right directory. configfile tells you which pytest configuration won. Pytest reads one configuration file, not a merged set of every config it finds. Use python -m pytest --collect-only -vv when the quiet list hides context. For the broader mechanics of discovery, see the pytest tutorial for beginners.

Root-Cause Decision Table

Symptom Root cause Fix
python -m pytest tests/ --collect-only works, plain invocation does not Wrong working directory or testpaths Start at the repository root; point testpaths at the actual suite
A file exists but never appears in the collection tree Filename misses test_*.py or *_test.py Rename the file or configure python_files
A module appears but contains no collected functions Function or class name misses discovery patterns Use test_ functions and Test classes; review custom patterns
A direct file path works but recursive search skips it norecursedirs, --ignore, or collect_ignore Remove the exclusion that matches that path
Unfiltered collection works but the usual command does not -k, -m, --deselect, or stale node ID Correct or remove the selector
Local collection works and CI or Docker finds nothing Different checkout, mount, working directory, or interpreter Inspect paths in the actual runtime and invoke the same Python
Normal tests collect but a plugin run does not Plugin-specific file pattern or command Install the needed plugin and confirm its collection convention

1. Fix pytest Collected 0 Items From the Wrong Working Directory

Pytest starts from the current directory when you give it no path and have no applicable testpaths. An IDE task, shell script, or CI job may start from a subdirectory that has no tests. First compare the shell location with the repository layout. The following tiny example creates a test using the default convention:

mkdir -p tests
printf 'def test_smoke():
    assert 2 + 2 == 4
' > tests/test_smoke.py
pwd
python -m pytest tests/test_smoke.py --collect-only -q

The final command should list tests/test_smoke.py::test_smoke and report one collected test. Now run python -m pytest --collect-only -q from the same repository root. If it finds zero, inspect the configfile line for a narrower testpaths setting. If you need to launch from elsewhere, pass an absolute test path or change into the project root before the command. Merely setting --rootdir does not change the directory searched and cannot repair an incorrect current directory.

Make the location explicit in automation. A shell script can derive the project directory from its own location, but only do that if the script has a stable home in the repo. In GitHub Actions, set the step's working-directory to the checkout path containing tests/. In an editor, check the run configuration's working directory, not just its displayed project name. A stable run location also makes relative fixture files behave consistently.

2. Fix pytest Collected 0 Items When Test Filenames Do Not Match

By default, recursive discovery looks for test_*.py and *_test.py. A file named checks.py, tests.py, or payment.spec.py can hold valid Python assertions yet be invisible to a normal scan. A direct file argument can reveal the mismatch: compare python -m pytest --collect-only -q with python -m pytest checks.py --collect-only -q when that file exists. Keep the normal naming convention if you control the suite:

mv tests/checks.py tests/test_checks.py
python -m pytest tests/ --collect-only -q

If another tool generates filenames such as check_*.py, configure only the additional pattern you need. Preserve the default pattern so existing tests still collect:

# pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py *_test.py check_*.py

Verify with python -m pytest --collect-only -q and look for a node ID from each naming family. python_files affects recursive discovery of Python modules; it does not turn a non-Python file into a pytest test. A typo in the extension, upper-case suffix on a case-sensitive filesystem, or a nested directory excluded by configuration can make a correctly named file remain unseen. If the direct path also fails, read the actual output rather than assuming naming is the only issue.

3. Repair Function and Class Names Inside a Collected File

When pytest lists a module but no test item under it, look inside the file. Top-level functions and methods normally start with test. Plain classes normally start with Test and must not define __init__. A helper called check_total is just a helper, even when it contains an assertion. Write a real test wrapper with a clear expected result:

# tests/test_totals.py
def total(items):
    return sum(items)


def test_total_adds_values():
    assert total([3, 4]) == 7


class TestEmptyCart:
    def test_total_is_zero(self):
        assert total([]) == 0

Check the exact items with python -m pytest tests/test_totals.py --collect-only -q. You should see test_total_adds_values and TestEmptyCart::test_total_is_zero, then python -m pytest tests/test_totals.py -q should pass both. Do not add an initializer to TestEmptyCart; use a fixture for setup if the class needs shared dependencies. The pytest versus unittest comparison explains why pytest setup differs from the class lifecycle familiar to unittest users.

Custom python_functions and python_classes patterns can exclude ordinary names. Inspect the active pytest.ini, pyproject.toml, or equivalent file if the example above still disappears. For unittest.TestCase subclasses, pytest delegates method discovery to unittest, so changing python_functions is not the appropriate repair. Also ensure the file defines callable tests at import time; a function nested inside another function is not a top-level test item.

4. Correct testpaths and the Active Configuration File

testpaths applies when pytest runs from its root without an explicit file or directory argument. A repo may move tests from testing/ to tests/ while leaving the old value behind. Confirm the header with python -m pytest --collect-only -vv, then inspect the named configfile. For a repository whose tests live in tests/, a minimal configuration is:

# pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py *_test.py

Run python -m pytest --collect-only -q from the directory containing pytest.ini. Compare it with python -m pytest tests/ --collect-only -q. Both should enumerate the same intended tests. If explicit tests/ succeeds and no-argument collection remains empty, verify that the file shown as configfile is the one you edited. A higher-priority or nearer configuration file may be taking effect. Inspect inherited addopts there too; a hidden selection option can make the result look like a path problem.

Do not spread the same settings across multiple files and expect pytest to merge them. Keep one authoritative project configuration and review changes to it alongside changes to test locations. rootdir helps interpret node IDs, but changing it alone does not make an absent testpaths directory appear. This distinction is especially useful in monorepos, where a test command may run from a package root or from the top-level workspace with different active configuration.

5. Remove Directory and File Exclusions That Hide Tests

A correctly named test can be skipped during recursion. Common sources are norecursedirs in config, command-line --ignore=PATH or --ignore-glob=PATTERN, and collect_ignore or collect_ignore_glob in conftest.py. Search for the exclusion that matches the real path:

python -m pytest tests/ --collect-only -q
python -m pytest tests/test_smoke.py --collect-only -q

If the direct file collects but a parent-directory scan does not, examine parent directory names and ignore rules. For example, this config intentionally prevents collection below generated_tests; remove that entry if the folder actually contains maintained tests:

# pytest.ini
[pytest]
testpaths = tests
norecursedirs = .git build dist generated_tests

After editing, verify using python -m pytest tests/ --collect-only -q and confirm that a node ID from the formerly skipped folder appears. Remember that defining norecursedirs replaces the default list, so retain exclusions your project still needs. Avoid making every Python file a test merely to bypass an ignore rule: that can import setup scripts and application modules unexpectedly. For a single file that should stay excluded, use a narrow pattern and document why. For a suite that belongs in a generated folder, decide whether generation happens before collection in every environment.

6. Remove Selectors That Filter Every Test

Selection happens after pytest has found candidate items. -k matches test names and related node names; -m selects marker expressions. --deselect excludes matching node IDs. A stale function name in an IDE run configuration or a changed marker spelling can leave zero selected tests even though unfiltered collection is healthy. Compare the plain run with the actual command:

python -m pytest tests/ --collect-only -q
python -m pytest tests/ -k smoke --collect-only -q
python -m pytest tests/ -m smoke --collect-only -q

The second command needs smoke in a matching node name. The third needs tests marked smoke; a filename alone does not create a marker. Here is a complete marked test:

# tests/test_health.py
import pytest


@pytest.mark.smoke
def test_service_health():
    assert True

Register the marker in pytest.ini with markers = smoke: fast health checks, then run python -m pytest tests/test_health.py -m smoke --collect-only -q and confirm one item. A custom addopts can silently inject selectors into every command; inspect it in the active config and check the PYTEST_ADDOPTS environment variable as well. If a node ID such as tests/test_health.py::test_old_name no longer exists, regenerate it with --collect-only -q rather than guessing. The guide to adding CI to a test framework shows why explicit selection is easier to audit in pipeline logs.

7. Make Sure the Intended Interpreter and Plugin Are Running

pytest on your shell path may belong to a different environment than the Python that contains the project dependencies. Use python -m pytest to bind pytest to the interpreter named by python, and print both paths when debugging:

python -c 'import sys, pytest; print(sys.executable); print(pytest.__file__)'
python -m pytest --version
python -m pytest tests/ --collect-only -q

The collection check should list the same test nodes in your local shell, virtual environment, and CI environment. If python -m pytest says No module named pytest, install dependencies into that interpreter using python -m pip install -r requirements.txt when your project has that file, or follow its declared package manager workflow. Avoid assuming that successful import of application code proves pytest is installed in the same interpreter.

Core pytest discovers Python tests; plugins may define additional file types and collection behavior. For example, pytest-bdd scenarios still require a Python test function decorated with the plugin's scenario API or its scenario generator. A .feature file by itself is not a core pytest test module. Verify the plugin is active with python -m pytest --trace-config, then collect the Python test wrapper. Follow the pytest-bdd tutorial when your feature files are the only artifacts you see. If importing a plugin raises an exception, treat that as a collection error, not as a zero-item naming failure; read the traceback before changing discovery settings.

8. Fix CI Collection Without Hiding Exit Code 5

A pipeline may check out a different branch, start in a package directory, omit generated tests, or pass an environment-specific marker. Add a short diagnostic step immediately before execution. In any shell-based CI runner, this block uses the same interpreter for diagnosis and the real run:

pwd
python -c 'import sys; print(sys.executable)'
python -m pytest --version
python -m pytest tests/ --collect-only -q
python -m pytest tests/ -q

For GitHub Actions, place the same lines under a run: | step and set working-directory to the checkout location that owns tests/. If the repository stores tests in a subproject, point both commands to that subproject's directory. Inspect checkout filters or sparse checkout settings when tests/ is absent. In a matrix job, print the matrix value next to the command so a marker or path built from it can be checked against actual node IDs.

A successful CI result must mean tests were collected and executed. Do not append || true or treat exit code 5 as success to silence the job. A test suite accidentally removed from a package should stop the release just as a failing assertion would. If your pipeline intentionally has a job with no tests, make that intent explicit in the job design and keep a separate collection gate for jobs that promise coverage. The CI troubleshooting interview guide covers related diagnosis patterns for environment drift.

9. Fix Docker Collection When Tests Are Missing From the Image

An image can contain pytest and application code while excluding tests/ through .dockerignore, an incomplete COPY, or a bind mount that replaces the image's working directory. Inspect the image, not just the host checkout. Use your already-built project image in these runnable commands:

PROJECT_IMAGE=your-project-image

docker run --rm "$PROJECT_IMAGE" pwd
docker run --rm "$PROJECT_IMAGE" python -c 'from pathlib import Path; print(Path("tests").exists())'
docker run --rm "$PROJECT_IMAGE" python -m pytest tests/ --collect-only -q

Replace your-project-image with the tag of your built image. The path check must print True, and the collection command must list node IDs. If it prints False, inspect .dockerignore, your Dockerfile's WORKDIR, and the stage used to run tests. A test stage needs the suite and its configuration inside the container, for example COPY tests/ tests/ alongside the project dependency files. A production stage may intentionally omit tests; run the test command against the test stage instead of weakening the production image.

For a mounted checkout, confirm the bind mount targets the same path as WORKDIR. Mounting an empty host folder over a populated /app hides image files during the run. Run docker run --rm -v "$PWD:/app" -w /app "$PROJECT_IMAGE" python -m pytest tests/ --collect-only -q only after confirming the host's current directory actually contains tests/. The Docker basics for testers explains how image layers, working directories, and mounts affect what pytest can see.

10. Diagnose Generated and Parameterized Tests

Some suites create test files or parameters during a build step. If generation did not run, pytest may genuinely have no eligible files. Check the expected output folder before changing python_files. If tests are created at collection time, make sure the hook or fixture that provides cases is loaded in the active environment. A simple parameterized test illustrates the expected behavior:

# tests/test_discount.py
import pytest


@pytest.mark.parametrize("price, expected", [(10, 9), (20, 18)])
def test_discount(price, expected):
    assert price * 0.9 == pytest.approx(expected)

Verify with python -m pytest tests/test_discount.py --collect-only -q; it should list two node IDs, one per parameter set. Then run python -m pytest tests/test_discount.py -q and expect two passes. If your parameter list comes from an external file, log its resolved path and count before collection, and fail clearly when it is unexpectedly empty. Passing an empty list to parametrization does not necessarily produce zero items: pytest may create a skipped placeholder item according to its empty parameter set behavior. Inspect the collection list and result instead of equating every skipped test with this error.

Generated Python tests still need a discovered path and a matching filename. Put generation before collection in CI and Docker, then assert the output directory contains the expected files. Keep generation deterministic so a local check and a pipeline run enumerate the same node IDs. If tests are selected dynamically by a plugin, collect once with the plugin enabled and once with its selection disabled to identify which stage removes items.

How to Verify the Fix

Verify both discovery and execution. First run python -m pytest --collect-only -q from the same directory and interpreter that the failing command used. Read the listed node IDs; a positive count alone can hide the fact that pytest found only an unrelated smoke test. Next run the exact CI, IDE, or shell command that previously returned zero. Remove --collect-only only after the intended set appears. Finally, check the process status and summary. A green assertion result with 1 passed or more confirms execution; collected 0 items and exit code 5 confirm the issue remains.

For a portable check in a Python-driven pipeline, use pytest's public exit code rather than parsing terminal text:

# verify_collection.py
import pytest


result = pytest.main(["tests/", "--collect-only", "-q"])
if result == pytest.ExitCode.NO_TESTS_COLLECTED:
    raise SystemExit("No tests were collected from tests/")
raise SystemExit(int(result))

Run python verify_collection.py. Exit 0 means collection succeeded, while any collection error preserves a nonzero status. This script checks that some tests exist, not that a particular suite is present. For a critical suite, review the node ID list or target its directory explicitly. If you use a custom configuration file, pass it with -c to both the diagnostic and the final invocation. Do not compare two commands that load different configs and infer a discovery fix from their different results.

Prevent It From Coming Back

Keep a small, stable smoke test in the intended directory and a collection-only check in CI. Review test renames alongside run configurations, marker expressions, and documentation; a stale -k expression can survive long after its matching test was deleted. Keep testpaths close to the source layout and avoid broad python_files patterns that import unrelated modules. Make the expected working directory explicit in scripts, CI jobs, and container commands.

When tests are split across packages, give each package a documented command and verify it separately. Record the interpreter path and pytest version in failed CI logs, but do not pin a guessed version to solve a discovery problem. If you change plugins or collection hooks, compare node IDs before and after the change. The test automation CI/CD guide provides a wider release-gate pattern for test execution. Treat an empty suite as a regression signal that deserves investigation even when the application build passes.

Interview Questions and Answers

Q: What does pytest exit code 5 mean?

It means no tests were collected. Explain which paths and selectors were active before proposing a fix, because the status does not say whether discovery or filtering emptied the run.

Q: How do you separate discovery from execution?

Use python -m pytest --collect-only -q to inspect node IDs without running test bodies. Then use the same path and filters without --collect-only to verify execution.

Q: Why might an explicit file path work while plain pytest finds nothing?

The file may live outside testpaths, have a nonstandard filename, or sit below a directory excluded from recursion. Check the active config and working directory before renaming anything.

Q: Does rootdir control imports or test search?

No. It is used for node IDs, cache location, and configuration context; it does not add imports to sys.path or override the current search location.

Q: How can -m smoke yield zero items?

The tests may lack the smoke marker, or the marker expression may be misspelled. Register the marker, apply it to tests, and confirm the selected node IDs with collection-only mode.

Q: What should CI do when the suite is empty?

Fail the job. Exit code 5 is a signal that the test gate did not run; suppressing it can make an untested release appear validated.

Common Mistakes

  • Treating collected 0 items as an import error. Import failures normally show a collection traceback; read the entire summary before editing names.
  • Renaming every file to match a broad pattern. Start with the exact missing path and keep application modules outside discovery.
  • Changing --rootdir to compensate for a wrong working directory. Pass the real test path or change directories instead.
  • Forgetting hidden selection in addopts or PYTEST_ADDOPTS. Reproduce with the full command and inspect environment values.
  • Fixing local collection while leaving CI's sparse checkout, Docker mount, or job directory unchanged. Verify where the failing runner actually executes.
  • Calling a skipped parameterized placeholder a zero-item run. Read the node list and exit status because the states differ.

Conclusion

Fix pytest collected 0 items by finding the first stage where the intended test disappears: start path, file match, test object, exclusion, or selector. Use --collect-only -q after each change, then run the exact original command and require a positive executed test count. Once CI and containers use the same suite path and interpreter, an empty run becomes a visible failure instead of a silent gap.

Interview Questions and Answers

What does collected 0 items mean in pytest?

It means pytest completed collection with no selected test items. The cause can be a wrong start path, naming mismatch, exclusion, or selection filter. I inspect node IDs with --collect-only before changing the suite.

How would you debug a suite that collects in a terminal but not in CI?

I print the CI working directory, Python executable, pytest version, active configuration, and test directory contents. I run collection-only using the exact CI path and selectors, then compare node IDs with the local run. This isolates checkout and environment differences.

What are pytest's default Python test naming rules?

Recursive discovery considers test_*.py and *_test.py modules. Within them, it collects test-prefixed functions and methods, including methods on Test-prefixed plain classes without an initializer. unittest.TestCase classes follow unittest's method discovery rules.

Why does an explicit test file collect while a parent directory does not?

A direct path bypasses some recursive filename and directory matching decisions. I check python_files, norecursedirs, --ignore, and conftest.py collect_ignore before editing the test body. The active configfile line tells me which settings to inspect.

How do you distinguish an empty suite from a failed import?

I read the summary and exit status. Empty collection is exit code 5; a module import failure normally prints a collection traceback and is reported as an error. Renaming files will not repair a missing dependency.

What is the role of testpaths?

It names the directories pytest searches when invoked without an explicit test path from the project root. A stale path can make a plain invocation empty while an explicit tests/ argument succeeds. I update the active configuration and verify both commands enumerate the intended suite.

How should a pipeline guard against accidental zero-test runs?

I keep pytest's nonzero status for no tests collected and add a collection-only gate before execution for important suites. I log node IDs or at least inspect them during a change to paths and filters. I never use || true on a test command that is supposed to validate a release.

Frequently Asked Questions

Why does pytest say collected 0 items when my test file exists?

The file may be outside the searched path, excluded by configuration, or named outside the default test_*.py and *_test.py patterns. Compare a direct file collection with a directory collection and inspect the active configfile.

What is the fastest way to debug no tests collected?

Run python -m pytest --collect-only -q from the intended working directory. Compare that list with python -m pytest tests/ --collect-only -q, then inspect the first point where the expected node ID disappears.

Is pytest exit code 5 a success?

No. It means pytest collected no tests. A CI job that promises test coverage should fail on that status and investigate why the suite was empty.

Does pytest require an __init__.py file in the tests directory?

No for ordinary pytest discovery. Package structure can affect imports and duplicate module names, but adding __init__.py is not the default remedy for a zero-item run.

Can a -k or -m filter cause collected 0 items?

Yes. A filter can deselect every candidate even when unfiltered discovery works. Run collection without the filter, then add each selector back and confirm the selected node IDs.

Why do tests collect locally but not in Docker?

The image may omit tests, start in a different WORKDIR, or have a bind mount hiding files. Inspect the container's current directory and test path, then run collection inside that same container.

Will changing --rootdir fix missing test discovery?

Usually not. Rootdir controls pytest's project context and node IDs, not the current directory searched for tests or Python imports. Pass an explicit test path or correct testpaths and the working directory.

Related Guides