[{"data":1,"prerenderedAt":1267},["ShallowReactive",2],{"page-\u002Fadvanced-pytest-architecture-configuration\u002Foptimizing-test-discovery\u002F":3},{"id":4,"title":5,"body":6,"description":133,"extension":1261,"meta":1262,"navigation":182,"path":1263,"seo":1264,"stem":1265,"__hash__":1266},"content\u002Fadvanced-pytest-architecture-configuration\u002Foptimizing-test-discovery\u002Findex.md","Optimizing Test Discovery",{"type":7,"value":8,"toc":1250},"minimark",[9,13,18,33,36,39,43,46,105,112,127,215,229,233,240,259,262,411,422,427,453,464,468,480,494,500,581,596,626,631,670,674,684,687,698,706,724,735,748,752,758,764,767,807,819,824,852,856,866,904,917,979,988,1053,1066,1091,1106,1110,1174,1178,1190,1217,1233,1246],[10,11,5],"h1",{"id":12},"optimizing-test-discovery",[14,15,17],"h2",{"id":16},"introduction-the-hidden-cost-of-test-collection","Introduction: The Hidden Cost of Test Collection",[19,20,21,22,26,27,32],"p",{},"In enterprise Python codebases, test execution latency is rarely the primary bottleneck. The true performance sink lies in the collection phase: the period between invoking ",[23,24,25],"code",{},"pytest"," and the actual execution of the first test case. Optimizing test discovery is foundational to ",[28,29,31],"a",{"href":30},"\u002Fadvanced-pytest-architecture-configuration\u002F","Advanced Pytest Architecture & Configuration",", as it dictates how efficiently your CI\u002FCD pipelines scale, how reliably your developer feedback loops operate, and how predictably your test infrastructure consumes compute resources.",[19,34,35],{},"Collection latency manifests when pytest traverses the filesystem, evaluates directory structures, parses Python modules, resolves fixture dependencies, and expands parametrized matrices. In monorepos exceeding 10,000 Python files, default discovery routines can consume 30 to 90 seconds before a single assertion runs. This overhead compounds exponentially when multiplied across parallel CI runners, pull request checks, and nightly regression suites. The distinction between discovery and execution latency is critical: execution scales linearly with test count and complexity, while discovery scales with repository topology, import graph density, and configuration overhead.",[19,37,38],{},"Baseline metrics for enterprise environments typically reveal that 15–40% of total pipeline runtime is spent in the collection phase. This is unacceptable for high-velocity teams targeting sub-5-minute feedback loops. Optimizing this phase requires shifting from reactive execution tuning to proactive architectural intervention. By intercepting pytest's hookspec lifecycle, replacing runtime imports with static analysis, and strategically pruning the collection graph, engineering teams can reduce startup overhead by 60–80%. The following sections deconstruct pytest's internal discovery pipeline, provide production-ready interception patterns, and outline profiling methodologies to systematically eliminate collection bottlenecks.",[14,40,42],{"id":41},"deconstructing-pytests-discovery-pipeline","Deconstructing Pytest's Discovery Pipeline",[19,44,45],{},"Pytest's collection phase operates as a deterministic, hook-driven state machine. Understanding its execution order is mandatory for targeted optimization. The pipeline progresses through five distinct phases:",[47,48,49,69,79,89,99],"ol",{},[50,51,52,56,57,60,61,64,65,68],"li",{},[53,54,55],"strong",{},"Filesystem Traversal & Pattern Matching:"," Pytest recursively walks the target directory tree, applying regex filters defined by ",[23,58,59],{},"python_files",", ",[23,62,63],{},"python_classes",", and ",[23,66,67],{},"python_functions",". This phase is I\u002FO bound and highly sensitive to directory depth.",[50,70,71,74,75,78],{},[53,72,73],{},"AST Parsing & Module Importation:"," For each matched file, pytest attempts to import the module. This triggers ",[23,76,77],{},"__init__.py"," execution, top-level imports, and module-level code evaluation.",[50,80,81,88],{},[53,82,83,84,87],{},"Class & Function Collection (",[23,85,86],{},"pytest_pycollect_makeitem","):"," Once imported, pytest inspects the module's namespace, identifying callable objects matching the configured naming patterns.",[50,90,91,94,95,98],{},[53,92,93],{},"Fixture Dependency Graph Resolution:"," Discovered items are analyzed for ",[23,96,97],{},"@pytest.fixture"," dependencies. Pytest constructs a directed acyclic graph (DAG) to determine setup\u002Fteardown ordering and scope boundaries.",[50,100,101,104],{},[53,102,103],{},"Parametrization Expansion & ID Generation:"," Parametrized tests are expanded into discrete test nodes, generating unique identifiers and computing Cartesian products for multi-dimensional parameter sets.",[19,106,107,108,111],{},"The default collection strategy relies heavily on runtime imports. When pytest encounters a test file, it executes ",[23,109,110],{},"importlib.import_module()",", which evaluates every top-level statement in the file and its transitive dependencies. In large repositories, this triggers massive import chains, database connection initializations, environment variable validations, and network calls—all during collection.",[19,113,114,115,118,119,122,123,126],{},"To mitigate this, engineers must intercept the pipeline before module execution occurs. The ",[23,116,117],{},"pytest_collect_file"," hook allows developers to inspect file paths and decide whether to delegate collection to the default ",[23,120,121],{},"PyFile"," collector. By combining this with ",[23,124,125],{},"pytest_ignore_collect",", teams can bypass entire directory trees or apply static analysis to determine test presence without triggering imports. Profiling this phase requires isolating collection from execution:",[128,129,134],"pre",{"className":130,"code":131,"language":132,"meta":133,"style":133},"language-bash shiki shiki-themes github-light github-dark","# Profile collection phase overhead\npython -m cProfile -o collection.prof -m pytest --collect-only -q\n\n# Analyze import chain bloat\npy-spy record --subprocesses -o profile.svg -- pytest --collect-only\n","bash","",[23,135,136,145,177,184,190],{"__ignoreMap":133},[137,138,141],"span",{"class":139,"line":140},"line",1,[137,142,144],{"class":143},"sJ8bj","# Profile collection phase overhead\n",[137,146,148,152,156,160,163,166,168,171,174],{"class":139,"line":147},2,[137,149,151],{"class":150},"sScJk","python",[137,153,155],{"class":154},"sj4cs"," -m",[137,157,159],{"class":158},"sZZnC"," cProfile",[137,161,162],{"class":154}," -o",[137,164,165],{"class":158}," collection.prof",[137,167,155],{"class":154},[137,169,170],{"class":158}," pytest",[137,172,173],{"class":154}," --collect-only",[137,175,176],{"class":154}," -q\n",[137,178,180],{"class":139,"line":179},3,[137,181,183],{"emptyLinePlaceholder":182},true,"\n",[137,185,187],{"class":139,"line":186},4,[137,188,189],{"class":143},"# Analyze import chain bloat\n",[137,191,193,196,199,202,204,207,210,212],{"class":139,"line":192},5,[137,194,195],{"class":150},"py-spy",[137,197,198],{"class":158}," record",[137,200,201],{"class":154}," --subprocesses",[137,203,162],{"class":154},[137,205,206],{"class":158}," profile.svg",[137,208,209],{"class":154}," --",[137,211,170],{"class":158},[137,213,214],{"class":154}," --collect-only\n",[19,216,217,218,221,222,225,226,228],{},"Monitoring ",[23,219,220],{},"sys.modules"," growth during ",[23,223,224],{},"--collect-only"," reveals unnecessary imports. If ",[23,227,220],{}," contains 500+ entries before any test runs, the collection pipeline is executing heavy initialization logic that should be deferred to fixture scopes.",[14,230,232],{"id":231},"static-analysis-bypassing-heavy-imports","Static Analysis & Bypassing Heavy Imports",[19,234,235,236,239],{},"Runtime imports during collection are the primary source of latency and memory bloat. By leveraging Python's built-in ",[23,237,238],{},"ast"," module, engineers can parse source files into abstract syntax trees and identify test functions or classes without executing module-level code. This technique, known as static collection interception, eliminates import-time side effects and drastically reduces memory footprint.",[19,241,242,243,245,246,249,250,253,254,258],{},"The ",[23,244,238],{}," module provides a safe, zero-execution parsing mechanism. By walking the syntax tree and inspecting ",[23,247,248],{},"ast.FunctionDef"," and ",[23,251,252],{},"ast.ClassDef"," nodes, we can determine whether a file contains tests matching pytest's naming conventions. This approach integrates seamlessly with ",[28,255,257],{"href":256},"\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002F","Mastering Pytest Fixtures"," by preventing premature fixture graph resolution. When pytest imports a module, it immediately evaluates fixture decorators and registers them in the internal registry. Static parsing defers this registration until the test is explicitly scheduled for execution, enabling lazy evaluation and graph pruning.",[19,260,261],{},"Below is a production-ready implementation of an AST-based static collector:",[128,263,266],{"className":264,"code":265,"language":151,"meta":133,"style":133},"language-python shiki shiki-themes github-light github-dark","import ast\nimport pytest\n\n@pytest.hookimpl(tryfirst=True)\ndef pytest_collect_file(path, parent):\n \"\"\"Intercept collection to parse Python files statically, bypassing heavy imports.\"\"\"\n if path.ext == \".py\":\n try:\n with open(path, \"r\", encoding=\"utf-8\") as f:\n tree = ast.parse(f.read())\n \n # Check for test functions or classes without importing the module\n has_tests = any(\n isinstance(node, (ast.FunctionDef, ast.ClassDef)) and\n node.name.startswith(\"test_\")\n for node in ast.walk(tree)\n )\n \n if has_tests:\n # Delegate to default PyFile collector only if tests exist\n return pytest.PyFile.from_parent(parent, path=path)\n except SyntaxError:\n # Fallback to default behavior for malformed files\n pass\n return None\n",[23,267,268,273,278,282,287,292,298,304,310,316,322,328,334,340,346,352,358,364,369,375,381,387,393,399,405],{"__ignoreMap":133},[137,269,270],{"class":139,"line":140},[137,271,272],{},"import ast\n",[137,274,275],{"class":139,"line":147},[137,276,277],{},"import pytest\n",[137,279,280],{"class":139,"line":179},[137,281,183],{"emptyLinePlaceholder":182},[137,283,284],{"class":139,"line":186},[137,285,286],{},"@pytest.hookimpl(tryfirst=True)\n",[137,288,289],{"class":139,"line":192},[137,290,291],{},"def pytest_collect_file(path, parent):\n",[137,293,295],{"class":139,"line":294},6,[137,296,297],{}," \"\"\"Intercept collection to parse Python files statically, bypassing heavy imports.\"\"\"\n",[137,299,301],{"class":139,"line":300},7,[137,302,303],{}," if path.ext == \".py\":\n",[137,305,307],{"class":139,"line":306},8,[137,308,309],{}," try:\n",[137,311,313],{"class":139,"line":312},9,[137,314,315],{}," with open(path, \"r\", encoding=\"utf-8\") as f:\n",[137,317,319],{"class":139,"line":318},10,[137,320,321],{}," tree = ast.parse(f.read())\n",[137,323,325],{"class":139,"line":324},11,[137,326,327],{}," \n",[137,329,331],{"class":139,"line":330},12,[137,332,333],{}," # Check for test functions or classes without importing the module\n",[137,335,337],{"class":139,"line":336},13,[137,338,339],{}," has_tests = any(\n",[137,341,343],{"class":139,"line":342},14,[137,344,345],{}," isinstance(node, (ast.FunctionDef, ast.ClassDef)) and\n",[137,347,349],{"class":139,"line":348},15,[137,350,351],{}," node.name.startswith(\"test_\")\n",[137,353,355],{"class":139,"line":354},16,[137,356,357],{}," for node in ast.walk(tree)\n",[137,359,361],{"class":139,"line":360},17,[137,362,363],{}," )\n",[137,365,367],{"class":139,"line":366},18,[137,368,327],{},[137,370,372],{"class":139,"line":371},19,[137,373,374],{}," if has_tests:\n",[137,376,378],{"class":139,"line":377},20,[137,379,380],{}," # Delegate to default PyFile collector only if tests exist\n",[137,382,384],{"class":139,"line":383},21,[137,385,386],{}," return pytest.PyFile.from_parent(parent, path=path)\n",[137,388,390],{"class":139,"line":389},22,[137,391,392],{}," except SyntaxError:\n",[137,394,396],{"class":139,"line":395},23,[137,397,398],{}," # Fallback to default behavior for malformed files\n",[137,400,402],{"class":139,"line":401},24,[137,403,404],{}," pass\n",[137,406,408],{"class":139,"line":407},25,[137,409,410],{}," return None\n",[19,412,413,414,417,418,421],{},"This hook operates at ",[23,415,416],{},"tryfirst=True"," priority, ensuring it executes before pytest's default file collection logic. If the file contains no test nodes, the hook returns ",[23,419,420],{},"None",", instructing pytest to skip the file entirely. This prevents the module from being imported, bypassing all top-level code execution.",[19,423,424],{},[53,425,426],{},"Architectural Trade-offs:",[428,429,430,443],"ul",{},[50,431,432,435,436,439,440,442],{},[53,433,434],{},"Pros:"," Eliminates import overhead, prevents ",[23,437,438],{},"conftest.py"," side effects, reduces ",[23,441,220],{}," pollution, scales linearly with file count rather than dependency graph complexity.",[50,444,445,448,449,452],{},[53,446,447],{},"Cons:"," Cannot detect dynamically generated tests (e.g., ",[23,450,451],{},"globals()[\"test_\" + name] = func","), requires AST parsing overhead (negligible compared to imports), may miss tests defined via metaclasses or decorators that modify names at runtime.",[19,454,455,456,459,460,463],{},"For maximum effectiveness, combine static parsing with fixture scope optimization. Session-scoped fixtures should avoid heavy initialization during collection. Instead, defer resource allocation to the ",[23,457,458],{},"setup"," phase using ",[23,461,462],{},"pytest.fixture(scope=\"session\", autouse=False)"," with explicit dependency injection.",[14,465,467],{"id":466},"custom-collection-hooks-plugin-architecture","Custom Collection Hooks & Plugin Architecture",[19,469,470,471,249,473,475,476,479],{},"Pytest's extensibility model allows engineers to override collection behavior through custom hooks and plugin registration. The ",[23,472,125],{},[23,474,117],{}," hooks form the backbone of selective collection strategies, enabling teams to bypass vendor directories, generated code, or legacy test suites without modifying ",[23,477,478],{},"pytest.ini"," configuration files.",[19,481,482,483,485,486,489,490,493],{},"When implementing custom collection logic, hook priority management is critical. Pytest evaluates hooks in registration order, with ",[23,484,416],{}," executing before defaults and ",[23,487,488],{},"trylast=True"," executing after. Misconfigured priorities can cause duplicate collection, infinite recursion, or silent test omissions. Proper plugin registration via ",[23,491,492],{},"entry_points"," ensures deterministic hook execution across environments.",[19,495,496,497,499],{},"The following pattern demonstrates selective collection using directory markers and ",[23,498,125],{},":",[128,501,503],{"className":264,"code":502,"language":151,"meta":133,"style":133},"import os\nimport pytest\nfrom pathlib import Path\n\n@pytest.hookimpl\ndef pytest_ignore_collect(collection_path, config):\n \"\"\"Bypass directories lacking explicit collection markers in monorepos.\"\"\"\n path_str = str(collection_path)\n \n # Skip vendor and generated directories by default\n if \"vendor\" in path_str or \"generated\" in path_str:\n marker_file = Path(collection_path.parent) \u002F \".pytest_collect\"\n # Only collect if marker file exists\n return not marker_file.exists()\n \n return False\n",[23,504,505,510,514,519,523,528,533,538,543,547,552,557,562,567,572,576],{"__ignoreMap":133},[137,506,507],{"class":139,"line":140},[137,508,509],{},"import os\n",[137,511,512],{"class":139,"line":147},[137,513,277],{},[137,515,516],{"class":139,"line":179},[137,517,518],{},"from pathlib import Path\n",[137,520,521],{"class":139,"line":186},[137,522,183],{"emptyLinePlaceholder":182},[137,524,525],{"class":139,"line":192},[137,526,527],{},"@pytest.hookimpl\n",[137,529,530],{"class":139,"line":294},[137,531,532],{},"def pytest_ignore_collect(collection_path, config):\n",[137,534,535],{"class":139,"line":300},[137,536,537],{}," \"\"\"Bypass directories lacking explicit collection markers in monorepos.\"\"\"\n",[137,539,540],{"class":139,"line":306},[137,541,542],{}," path_str = str(collection_path)\n",[137,544,545],{"class":139,"line":312},[137,546,327],{},[137,548,549],{"class":139,"line":318},[137,550,551],{}," # Skip vendor and generated directories by default\n",[137,553,554],{"class":139,"line":324},[137,555,556],{}," if \"vendor\" in path_str or \"generated\" in path_str:\n",[137,558,559],{"class":139,"line":330},[137,560,561],{}," marker_file = Path(collection_path.parent) \u002F \".pytest_collect\"\n",[137,563,564],{"class":139,"line":336},[137,565,566],{}," # Only collect if marker file exists\n",[137,568,569],{"class":139,"line":342},[137,570,571],{}," return not marker_file.exists()\n",[137,573,574],{"class":139,"line":348},[137,575,327],{},[137,577,578],{"class":139,"line":354},[137,579,580],{}," return False\n",[19,582,583,584,587,588,591,592,595],{},"This implementation leverages a sentinel file (",[23,585,586],{},".pytest_collect",") to opt-in specific subdirectories within excluded paths. This is particularly valuable in polyglot monorepos where Python tests coexist with Go, Rust, or JavaScript tooling. The hook returns ",[23,589,590],{},"True"," to ignore the path, ",[23,593,594],{},"False"," to proceed with default collection.",[19,597,598,599,60,602,605,606,609,610,613,614,618,619,621,622,625],{},"For non-standard file extensions (e.g., ",[23,600,601],{},".test.py",[23,603,604],{},".spec.py","), engineers can register custom collectors by subclassing ",[23,607,608],{},"pytest.PyCollector"," and implementing ",[23,611,612],{},"collect()",". Reference ",[28,615,617],{"href":616},"\u002Fadvanced-pytest-architecture-configuration\u002Fbuilding-custom-pytest-plugins\u002F","Building Custom Pytest Plugins"," for comprehensive guidance on ",[23,620,492],{}," configuration, ",[23,623,624],{},"pyproject.toml"," plugin declarations, and hookspec lifecycle management.",[19,627,628],{},[53,629,630],{},"Plugin Architecture Best Practices:",[428,632,633,645,652,663],{},[50,634,635,636,638,639,641,642,644],{},"Always return ",[23,637,420],{}," from ",[23,640,117],{}," when a file does not match your criteria. Returning ",[23,643,594],{}," or raising exceptions breaks pytest's collection chain.",[50,646,647,648,651],{},"Use ",[23,649,650],{},"config.getini(\"python_files\")"," to respect user-defined patterns in custom collectors.",[50,653,654,655,658,659,662],{},"Cache AST parse results using ",[23,656,657],{},"functools.lru_cache"," or ",[23,660,661],{},"pytest_cache"," to avoid redundant parsing across test runs.",[50,664,665,666,669],{},"Validate hook implementations with ",[23,667,668],{},"pytest --traceconfig"," to verify registration order and priority resolution.",[14,671,673],{"id":672},"parallelization-strategies-discovery-vs-execution","Parallelization Strategies: Discovery vs Execution",[19,675,676,677,249,680,683],{},"A pervasive misconception in pytest optimization is the assumption that parallel execution frameworks parallelize test discovery. They do not. ",[23,678,679],{},"pytest-xdist",[23,681,682],{},"pytest-parallel"," distribute test execution across worker processes, but collection remains strictly single-threaded on the master process. This architectural constraint means that optimizing discovery is orthogonal to parallelizing execution.",[19,685,686],{},"When workers are spawned, each process independently imports modules and resolves fixtures. If collection is inefficient, every worker inherits the same startup penalty, multiplying latency by the number of workers. The master process must complete collection before distributing test IDs to workers, creating a hard serialization bottleneck.",[19,688,689,690,693,694,697],{},"Worker distribution modes interact differently with discovery caching. The ",[23,691,692],{},"--dist=loadscope"," strategy groups tests by fixture scope to minimize setup\u002Fteardown cycles, but it requires the master to fully resolve the fixture dependency graph before scheduling. Conversely, ",[23,695,696],{},"--dist=loadfile"," distributes tests by file path, which can reduce graph resolution overhead but increases fixture duplication across workers.",[19,699,700,701,705],{},"For large test matrices, reference the ",[28,702,704],{"href":703},"\u002Fadvanced-pytest-architecture-configuration\u002Foptimizing-test-discovery\u002Fpytest-xdist-vs-pytest-parallel-performance-comparison\u002F","pytest-xdist vs pytest-parallel performance comparison"," to select optimal distribution modes. In practice:",[428,707,708,713,718],{},[50,709,647,710,712],{},[23,711,692],{}," when session-scoped fixtures dominate (e.g., database connections, Docker containers).",[50,714,647,715,717],{},[23,716,696],{}," when tests are I\u002FO bound and fixture dependencies are minimal.",[50,719,647,720,723],{},[23,721,722],{},"--dist=worksteal"," for highly heterogeneous test durations to prevent worker starvation.",[19,725,726,727,730,731,734],{},"Collection caching mitigates repeated discovery overhead. Pytest's ",[23,728,729],{},".pytest_cache"," directory stores collection metadata, including file hashes and test node IDs. When running ",[23,732,733],{},"pytest --collect-only",", the cache can be leveraged to skip unchanged files. However, cache invalidation must be carefully managed in CI environments to prevent stale collection graphs.",[19,736,737,740,741,743,744,747],{},[53,738,739],{},"Network Serialization Overhead:"," When distributing tests across remote workers (e.g., ",[23,742,679],{}," with ",[23,745,746],{},"--tx","), test IDs and fixture parameters are serialized over sockets. Large parametrized matrices can generate megabytes of metadata, increasing network latency. Compressing test IDs or reducing parametrization dimensions during collection improves distribution throughput.",[14,749,751],{"id":750},"mitigating-flakiness-during-collection","Mitigating Flakiness During Collection",[19,753,754,755,757],{},"Collection-phase flakiness manifests when environment-dependent code executes during module import. Common culprits include missing environment variables, network calls in ",[23,756,438],{},", database migrations triggered at import time, or platform-specific library loading. Because discovery runs before test execution, these failures appear as collection errors rather than test failures, making them notoriously difficult to debug.",[19,759,760,761,763],{},"Import-time side effects violate pytest's separation of concerns. Collection should be deterministic, stateless, and environment-agnostic. Heavy setup must be deferred to fixtures with explicit scopes. When ",[23,762,438],{}," contains top-level code that initializes connections or validates configurations, it executes during every collection cycle, regardless of whether the tests in that directory are actually scheduled.",[19,765,766],{},"To isolate collection from execution state:",[47,768,769,779,785,796],{},[50,770,771,772,743,775,778],{},"Wrap initialization logic in ",[23,773,774],{},"@pytest.fixture(scope=\"session\")",[23,776,777],{},"autouse=False",".",[50,780,647,781,784],{},[23,782,783],{},"pytest.importorskip()"," to gracefully handle missing optional dependencies during collection.",[50,786,787,788,791,792,795],{},"Validate environment variables using ",[23,789,790],{},"os.environ.get()"," with explicit defaults rather than raising ",[23,793,794],{},"KeyError"," at module level.",[50,797,798,799,802,803,806],{},"Defer network calls to ",[23,800,801],{},"setup_module"," or fixture ",[23,804,805],{},"yield"," blocks.",[19,808,809,810,814,815,818],{},"When collection flakiness persists, post-collection retry strategies can mask transient failures. Reference ",[28,811,813],{"href":812},"\u002Fadvanced-pytest-architecture-configuration\u002Foptimizing-test-discovery\u002Fdebugging-flaky-tests-with-pytest-rerunfailures\u002F","Debugging flaky tests with pytest-rerunfailures"," for implementing robust retry mechanisms. However, note that ",[23,816,817],{},"pytest-rerunfailures"," operates at the execution phase. Collection errors cannot be retried without modifying the collection pipeline itself.",[19,820,821],{},[53,822,823],{},"State Leakage Prevention:",[428,825,826,833,843],{},[50,827,828,829,832],{},"Avoid global mutable state in test modules. Use ",[23,830,831],{},"pytest.mark.parametrize"," with explicit data rather than module-level dictionaries.",[50,834,835,836,838,839,842],{},"Clear ",[23,837,220],{}," entries for dynamically loaded test modules using ",[23,840,841],{},"importlib.invalidate_caches()"," when implementing hot-reload collection.",[50,844,647,845,249,848,851],{},[23,846,847],{},"pytest.warns()",[23,849,850],{},"pytest.raises()"," at the test level, not during collection.",[14,853,855],{"id":854},"large-repository-optimization-startup-profiling","Large Repository Optimization & Startup Profiling",[19,857,858,859,862,863,865],{},"Scaling pytest to monorepo environments requires systematic profiling and configuration tuning. The default recursive traversal algorithm performs poorly when directory depth exceeds 15 levels or when ",[23,860,861],{},"norecursedirs"," is misconfigured. Tactical optimization begins with ",[23,864,224],{}," profiling to establish baseline metrics.",[128,867,869],{"className":130,"code":868,"language":132,"meta":133,"style":133},"# Measure collection time with verbose node reporting\ntime pytest --collect-only -q --no-header\n\n# Profile hook execution durations\npytest --collect-only --durations=0\n",[23,870,871,876,886,890,895],{"__ignoreMap":133},[137,872,873],{"class":139,"line":140},[137,874,875],{"class":143},"# Measure collection time with verbose node reporting\n",[137,877,878,882],{"class":139,"line":147},[137,879,881],{"class":880},"szBVR","time",[137,883,885],{"class":884},"sVt8B"," pytest --collect-only -q --no-header\n",[137,887,888],{"class":139,"line":179},[137,889,183],{"emptyLinePlaceholder":182},[137,891,892],{"class":139,"line":186},[137,893,894],{"class":143},"# Profile hook execution durations\n",[137,896,897,899,901],{"class":139,"line":192},[137,898,25],{"class":150},[137,900,173],{"class":154},[137,902,903],{"class":154}," --durations=0\n",[19,905,242,906,909,910,658,912,914,915,499],{},[23,907,908],{},"--durations=0"," flag reports the time spent in each collection hook, revealing bottlenecks in ",[23,911,117],{},[23,913,86],{},". If collection exceeds 10 seconds, implement directory exclusion patterns in ",[23,916,478],{},[128,918,922],{"className":919,"code":920,"language":921,"meta":133,"style":133},"language-ini shiki shiki-themes github-light github-dark","[pytest]\nnorecursedirs = \n .git\n .tox\n venv\n node_modules\n vendor\n *.egg-info\n __pycache__\n migrations\n generated\n","ini",[23,923,924,929,934,939,944,949,954,959,964,969,974],{"__ignoreMap":133},[137,925,926],{"class":139,"line":140},[137,927,928],{},"[pytest]\n",[137,930,931],{"class":139,"line":147},[137,932,933],{},"norecursedirs = \n",[137,935,936],{"class":139,"line":179},[137,937,938],{}," .git\n",[137,940,941],{"class":139,"line":186},[137,942,943],{}," .tox\n",[137,945,946],{"class":139,"line":192},[137,947,948],{}," venv\n",[137,950,951],{"class":139,"line":294},[137,952,953],{}," node_modules\n",[137,955,956],{"class":139,"line":300},[137,957,958],{}," vendor\n",[137,960,961],{"class":139,"line":306},[137,962,963],{}," *.egg-info\n",[137,965,966],{"class":139,"line":312},[137,967,968],{}," __pycache__\n",[137,970,971],{"class":139,"line":318},[137,972,973],{}," migrations\n",[137,975,976],{"class":139,"line":324},[137,977,978],{}," generated\n",[19,980,981,982,984,985,987],{},"Leverage ",[23,983,729],{}," to persist collection metadata across CI runs. Configure your CI pipeline to cache the ",[23,986,729],{}," directory between stages:",[128,989,993],{"className":990,"code":991,"language":992,"meta":133,"style":133},"language-yaml shiki shiki-themes github-light github-dark","# GitHub Actions example\n- name: Cache pytest collection\n uses: actions\u002Fcache@v3\n with:\n path: .pytest_cache\n key: pytest-cache-${{ runner.os }}-${{ hashFiles('**\u002Fpytest.ini', '**\u002Fconftest.py') }}\n","yaml",[23,994,995,1000,1015,1025,1033,1043],{"__ignoreMap":133},[137,996,997],{"class":139,"line":140},[137,998,999],{"class":143},"# GitHub Actions example\n",[137,1001,1002,1005,1009,1012],{"class":139,"line":147},[137,1003,1004],{"class":884},"- ",[137,1006,1008],{"class":1007},"s9eBZ","name",[137,1010,1011],{"class":884},": ",[137,1013,1014],{"class":158},"Cache pytest collection\n",[137,1016,1017,1020,1022],{"class":139,"line":179},[137,1018,1019],{"class":1007}," uses",[137,1021,1011],{"class":884},[137,1023,1024],{"class":158},"actions\u002Fcache@v3\n",[137,1026,1027,1030],{"class":139,"line":186},[137,1028,1029],{"class":1007}," with",[137,1031,1032],{"class":884},":\n",[137,1034,1035,1038,1040],{"class":139,"line":192},[137,1036,1037],{"class":1007}," path",[137,1039,1011],{"class":884},[137,1041,1042],{"class":158},".pytest_cache\n",[137,1044,1045,1048,1050],{"class":139,"line":294},[137,1046,1047],{"class":1007}," key",[137,1049,1011],{"class":884},[137,1051,1052],{"class":158},"pytest-cache-${{ runner.os }}-${{ hashFiles('**\u002Fpytest.ini', '**\u002Fconftest.py') }}\n",[19,1054,1055,1056,1058,1059,1062,1063,1065],{},"Import chain optimization requires auditing ",[23,1057,220],{}," during collection. Use ",[23,1060,1061],{},"python -c \"import sys; print(len(sys.modules))\""," before and after ",[23,1064,733],{}," to quantify import pollution. Reduce transitive dependencies by:",[428,1067,1068,1075,1082],{},[50,1069,1070,1071,1074],{},"Replacing ",[23,1072,1073],{},"from package import *"," with explicit imports.",[50,1076,1077,1078,1081],{},"Using ",[23,1079,1080],{},"importlib.util.find_spec()"," to conditionally load heavy modules.",[50,1083,1084,1085,1088,1089,778],{},"Implementing lazy imports via ",[23,1086,1087],{},"__getattr__"," in ",[23,1090,77],{},[19,1092,1093,1094,1097,1098,1101,1102,1105],{},"For comprehensive startup reduction strategies, consult Optimizing pytest startup time in large repos. Key takeaways include leveraging ",[23,1095,1096],{},"PYTHONOPTIMIZE=1"," to strip docstrings and assertions during collection, using ",[23,1099,1100],{},"site.addsitedir()"," to preload critical paths, and configuring ",[23,1103,1104],{},"PYTHONDONTWRITEBYTECODE=1"," in ephemeral CI runners to reduce disk I\u002FO.",[14,1107,1109],{"id":1108},"common-pitfalls-in-test-collection","Common Pitfalls in Test Collection",[47,1111,1112,1130,1139,1153,1162],{},[50,1113,1114,1123,1124,1126,1127,1129],{},[53,1115,1116,1117,1119,1120,1122],{},"Overriding ",[23,1118,117],{}," without returning ",[23,1121,420],{}," for non-matching paths:"," Returning ",[23,1125,594],{}," or omitting a return statement causes pytest to fall back to default collection, resulting in duplicate nodes or infinite recursion. Always explicitly return ",[23,1128,420],{}," when a file should be skipped.",[50,1131,1132,1138],{},[53,1133,1134,1135,1137],{},"Placing heavy initialization in ",[23,1136,438],{}," module scope:"," Database connections, HTTP clients, or file I\u002FO at the top level execute during discovery. This triggers unnecessary setup\u002Fteardown cycles and inflates memory usage. Defer to session-scoped fixtures.",[50,1140,1141,1148,1149,1152],{},[53,1142,1143,1144,1088,1146,499],{},"Misconfiguring ",[23,1145,861],{},[23,1147,478],{}," Incorrect glob patterns or missing trailing slashes lead to silent test omissions. Validate exclusion rules with ",[23,1150,1151],{},"pytest --collect-only --co"," and verify node counts match expectations.",[50,1154,1155,1161],{},[53,1156,1157,1158,1160],{},"Assuming ",[23,1159,679],{}," parallelizes discovery:"," Collection remains single-threaded. Parallel execution only begins after the master completes discovery. Optimize collection first, then scale execution.",[50,1163,1164,1169,1170,1173],{},[53,1165,1077,1166,1168],{},[23,1167,831],{}," with expensive generators:"," Generators evaluate during collection, not execution. Replace with ",[23,1171,1172],{},"pytest.lazy_fixture"," or compute parameters inside the test function to defer evaluation.",[14,1175,1177],{"id":1176},"frequently-asked-questions","Frequently Asked Questions",[19,1179,1180,1183,1184,1186,1187,1189],{},[53,1181,1182],{},"Does pytest-xdist parallelize test discovery?","\nNo. pytest-xdist parallelizes test execution across worker processes. Discovery remains single-threaded on the master process. To optimize discovery, use static AST parsing, ",[23,1185,224],{}," caching, or custom ",[23,1188,125],{}," hooks.",[19,1191,1192,1195,1196,1199,1200,658,1203,1205,1206,1209,1210,1212,1213,1216],{},[53,1193,1194],{},"How can I profile pytest's collection phase?","\nRun ",[23,1197,1198],{},"pytest --collect-only -v"," combined with Python's ",[23,1201,1202],{},"cProfile",[23,1204,195],{},". Use ",[23,1207,1208],{},"pytest --durations=0"," to identify slow collection hooks, and monitor ",[23,1211,220],{}," growth to detect unnecessary imports. For granular hook timing, implement a custom ",[23,1214,1215],{},"pytest_runtest_logstart"," wrapper that logs timestamps before and after collection.",[19,1218,1219,1222,1223,1225,1226,60,1229,1232],{},[53,1220,1221],{},"Why are my tests failing during discovery but passing when run individually?","\nThis typically indicates import-time side effects in ",[23,1224,438],{}," or test modules. Discovery executes module-level code. Isolate heavy setup into fixtures with explicit scopes (",[23,1227,1228],{},"session",[23,1230,1231],{},"module",") to defer execution. Ensure environment variables and external dependencies are validated lazily rather than at import time.",[19,1234,1235,1238,1239,1241,1242,1245],{},[53,1236,1237],{},"Can I cache test discovery results across CI runs?","\nYes. Pytest's ",[23,1240,729],{}," directory stores collection metadata. Ensure ",[23,1243,1244],{},"--cache-clear"," is not used unnecessarily, and configure CI to persist the cache directory between pipeline stages. Note that cache invalidation occurs automatically when file hashes change, but custom collection hooks may require manual cache management to prevent stale node resolution.",[1247,1248,1249],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":133,"searchDepth":147,"depth":147,"links":1251},[1252,1253,1254,1255,1256,1257,1258,1259,1260],{"id":16,"depth":147,"text":17},{"id":41,"depth":147,"text":42},{"id":231,"depth":147,"text":232},{"id":466,"depth":147,"text":467},{"id":672,"depth":147,"text":673},{"id":750,"depth":147,"text":751},{"id":854,"depth":147,"text":855},{"id":1108,"depth":147,"text":1109},{"id":1176,"depth":147,"text":1177},"md",{},"\u002Fadvanced-pytest-architecture-configuration\u002Foptimizing-test-discovery",{"title":5,"description":133},"advanced-pytest-architecture-configuration\u002Foptimizing-test-discovery\u002Findex","CBVjIW60GoI9Sqa7AqC65gYQHZbXKURWHvWvCN_6FaA",1778004578487]