Repository navigation
Collection is quadratic in the number of file arguments from one directory: each file argument re-collects its parent directory #15187
Description
Description
When several test files from the same directory are passed as separate command-line arguments, Session.collect re-collects the parent directory once per argument. Each re-collection lists the directory again and offers every entry to pytest_ignore_collect / pytest_collect_file again. Cost is O(files given x entries in the directory).
Passing the directory instead, or the same files through a single directory argument, does not have the problem.
We hit this in CI, where a job passes ~100 explicit test files from a directory with ~940 entries: each xdist worker spent 10-16 s of its 22-33 s collection re-walking that one directory. Keeping the cache for directory nodes (below) gave identical collected node IDs and roughly halved collection time.
Minimal reproduction
mkdir tests
cat > tests/conftest.py <<'EOF'
import collections
calls = collections.Counter()
def pytest_collect_file(file_path, parent):
calls[file_path.name] += 1
def pytest_collection_finish(session):
print(f"\npytest_collect_file calls: {sum(calls.values())} "
f"(test_0000.py offered {calls['test_0000.py']} times)")
EOF
for i in $(seq -f '%04g' 0 299); do printf 'def test_%s():\n pass\n' "$i" > "tests/test_$i.py"; done
python -m pytest --collect-only -q -p no:cacheprovider tests # directory argument
python -m pytest --collect-only -q -p no:cacheprovider tests/test_*.py # 300 file argumentsOutput (pytest 9.1.1, CPython 3.12, macOS; same on Linux with 9.0.3):
-- directory argument
pytest_collect_file calls: 301 (test_0000.py offered 1 times)
300 tests collected in 0.12s
-- 300 file arguments
pytest_collect_file calls: 90300 (test_0000.py offered 300 times)
300 tests collected in 3.97s
Same 300 tests both ways; 300x the hook calls and ~33x the time with file arguments.
Cause
src/_pytest/main.py, Session.collect (unchanged from 8.x through 9.1.1 and current main):
# For backward compat, files given directly multiple
# times on the command line should not be deduplicated.
handle_dupes = not (
len(matchparts) == 1
and isinstance(matchparts[0], Path)
and matchparts[0].is_file()
)
rep, duplicate = self._collect_one_node(matchnode, handle_dupes)When the only remaining part is a file, matchnode is that file's PARENT Directory/Package, so handle_dupes=False bypasses _collection_cache for the directory, not for the file. The intent (a file named twice runs twice) only needs the FILE not to be deduplicated, which genitems already ensures (handle_dupes = not isinstance(node, nodes.File)).
Possible fix
Use the cache for directory nodes in that branch and leave dedup behaviour for files alone, e.g.:
handle_dupes = (
isinstance(matchnode, nodes.Directory) and not self.config.option.keepduplicates
) or not (
len(matchparts) == 1 and isinstance(matchparts[0], Path) and matchparts[0].is_file()
)One caveat we hit: under --keep-duplicates, a file named twice needs two separate Module nodes (two setup_module/teardown_module cycles), which today only the directory re-collection provides. So the directory cache has to stay off in that mode, or the duplicate handling has to move to the file node. Without --keep-duplicates, duplicate paths are already removed during argument normalisation, so nothing depends on the re-walk.
We run this as a monkeypatch of Session._collect_one_node (force handle_dupes=True for nodes.Directory). Across ~17k tests in ~480 files, with files repeated and node-ID arguments mixed in, the collected node IDs are identical with and without it. It would also make the #14635 reconciliation unnecessary for this path, since the directory is no longer re-collected. Happy to open a PR with a test if this direction is acceptable.
Versions
- pytest 9.0.3 and 9.1.1 (code identical on
mainas of 2026-10-15) - CPython 3.12
- Linux (GitHub Actions, ubuntu-latest) and macOS
This report was drafted with help from an AI coding agent; the repro and numbers were run and checked by us.
Activity
dannyward630 commentedon Oct 15, 2026
I’m preparing a focused change to avoid re-collecting parent directories for distinct file arguments while preserving the existing
--keep-duplicatesbehavior, with regression coverage. Assisted by Codex (GPT-6).PrithwishDas101 commentedon Oct 18, 2026
I traced this to the "handle_dupes" decision in "Session.collect()" in "src/_pytest/main.py".
For a direct file argument, the current condition disables duplicate handling on the current collector. At this point in the traversal, that collector can be the parent "Directory" or "Package", so every requested file can cause the same directory to be collected again.
I think the fix should distinguish caching the parent directory from deduplicating the requested file. When "--keep-duplicates" is disabled, argument normalization has already removed duplicate and overlapping paths, so the parent directory's collection report can be reused. The requested "File" collector can still be recollected through the existing "genitems()" path, which disables duplicate handling for file nodes.
When "--keep-duplicates" is enabled, retain the current directory re-collection behavior to avoid changing the semantics of repeated file arguments.
I'd add a regression test with many individual files in one directory, asserting that collection-hook calls no longer grow quadratically, plus a test confirming that repeated file arguments still behave correctly with "--keep-duplicates".
I haven't run this patch yet, so the test results and compatibility with custom collectors still need verification.
dannyward630 commentedon Oct 19, 2026
I found open PR #15194 covering the same approach, including preserving
--keep-duplicatesbehavior. Its proposed semantics and regression coverage match this issue, so I won’t submit a duplicate. I’ll revisit only if review identifies a gap. Assisted by Codex (GPT-6).Fix quadratic collection cost for multiple file args from one directory
Add regression test for parent directory re-collection (pytest-dev#15187
Add changelog entry for pytest-dev#15187