[{"data":1,"prerenderedAt":1523},["ShallowReactive",2],{"page-\u002Fadvanced-pytest-architecture-configuration\u002Ftest-layout-and-import-modes\u002F":3},{"id":4,"title":5,"body":6,"description":1481,"extension":1482,"meta":1483,"navigation":493,"path":1519,"seo":1520,"stem":1521,"__hash__":1522},"content\u002Fadvanced-pytest-architecture-configuration\u002Ftest-layout-and-import-modes\u002Findex.md","Test Layout & Import Modes",{"type":7,"value":8,"toc":1458},"minimark",[9,24,29,66,70,73,104,125,253,257,262,271,286,290,321,330,334,370,376,384,396,400,452,458,461,465,468,540,591,597,601,712,716,729,758,788,803,903,907,912,932,1037,1064,1068,1071,1084,1090,1096,1119,1123,1126,1132,1149,1162,1172,1176,1179,1246,1262,1265,1269,1272,1285,1298,1308,1312,1315,1332,1335,1339,1357,1369,1388,1400,1409,1413,1447,1454],[10,11,12,16,17,16,20,23],"p",{},[13,14,15],"code",{},"ImportError while loading conftest",", ",[13,18,19],{},"import file mismatch",[13,21,22],{},"ModuleNotFoundError: No module named 'myapp'",", a test that passes locally and fails in CI because a different copy of the package was imported: all four are the same problem wearing different hats. pytest's import behaviour is a small set of rules interacting with the shape of the repository, and once the rules are explicit the errors stop being mysterious.",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,32,43,50,57],"ul",{},[33,34,35,38,39,42],"li",{},[13,36,37],{},"pytest >= 8.0","; ",[13,40,41],{},"importmode=importlib"," has existed since 6.0 but became reliably usable for most layouts in 8.",[33,44,45,46,49],{},"A packaging setup that can install the project — ",[13,47,48],{},"pyproject.toml"," with any modern backend.",[33,51,52,53,56],{},"Python 3.9+, and an understanding of what ",[13,54,55],{},"sys.path"," ordering means for imports.",[33,58,59,60,65],{},"The configuration file hierarchy covered in ",[61,62,64],"a",{"href":63},"\u002Fadvanced-pytest-architecture-configuration\u002Fpytest-configuration-best-practices\u002Fpyproject-toml-vs-pytest-ini\u002F","pyproject.toml vs pytest.ini",".",[25,67,69],{"id":68},"core-concept-two-decisions-four-common-outcomes","Core concept: two decisions, four common outcomes",[10,71,72],{},"Everything here follows from two independent choices: where the package sits relative to the repository root, and which import mode pytest uses.",[10,74,75,79,80,84,85,88,89,92,93,96,97,100,101,103],{},[76,77,78],"strong",{},"Layout."," A ",[81,82,83],"em",{},"flat"," layout puts ",[13,86,87],{},"myapp\u002F"," at the repository root next to ",[13,90,91],{},"tests\u002F",". A ",[81,94,95],{},"src"," layout puts it at ",[13,98,99],{},"src\u002Fmyapp\u002F",". The difference is whether the repository root — which pytest and Python both tend to put on ",[13,102,55],{}," — contains an importable copy of the package.",[10,105,106,109,110,113,114,117,118,121,122,124],{},[76,107,108],{},"Import mode."," Under ",[13,111,112],{},"prepend",", pytest inserts the test file's rootdir-relative first non-package parent into ",[13,115,116],{},"sys.path[0]"," and imports the module under a name derived from its path. Under ",[13,119,120],{},"importlib",", pytest imports the file directly through the import system without touching ",[13,123,55],{}," at all.",[126,127,130,249],"figure",{"className":128},[129],"diagram",[131,132,139,140,139,144,139,148,139,156,139,166,139,171,139,175,139,180,139,184,139,194,139,198,139,203,139,207,139,212,139,215,139,220,139,224,139,227,139,230,139,234,139,237,139,241,139,244,139,246],"svg",{"viewBox":133,"role":134,"ariaLabelledBy":135,"xmlns":138},"0 0 840 300","img",[136,137],"layout-t","layout-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[141,142,143],"title",{"id":136},"Layout and import mode as independent choices",[145,146,147],"desc",{"id":137},"A two-by-two grid. Flat layout with prepend mode is the default and shadows the installed package while colliding on duplicate basenames. Flat layout with importlib removes the collisions but still shadows. Src layout with prepend avoids shadowing but still collides. Src layout with importlib avoids both problems.",[149,150],"rect",{"x":151,"y":151,"width":152,"height":153,"rx":154,"fill":155},"0","840","300","14","#fffdf8",[157,158,165],"text",{"x":159,"y":160,"textAnchor":161,"fontSize":162,"fontWeight":163,"fill":164},"420","28","middle","16","700","#3d405b","Two choices, and only one combination has neither problem",[157,167,170],{"x":153,"y":168,"textAnchor":161,"fontSize":169,"fontWeight":163,"fill":164},"66","12","importmode = prepend",[157,172,174],{"x":173,"y":168,"textAnchor":161,"fontSize":169,"fontWeight":163,"fill":164},"620","importmode = importlib",[157,176,179],{"x":177,"y":178,"textAnchor":161,"fontSize":169,"fontWeight":163,"fill":164},"96","130","flat layout",[157,181,183],{"x":177,"y":182,"textAnchor":161,"fontSize":169,"fontWeight":163,"fill":164},"230","src layout",[149,185],{"x":186,"y":187,"width":188,"height":189,"rx":190,"fill":191,"stroke":192,"strokeWidth":193},"176","80","248","94","11","#fbe9e3","#e07a5f","2",[157,195,197],{"x":153,"y":196,"textAnchor":161,"fontSize":169,"fontWeight":163,"fill":164},"106","the default",[157,199,202],{"x":200,"y":178,"fontSize":190,"fill":201},"194","#8f3d22","✗ shadows the installed copy",[157,204,206],{"x":200,"y":205,"fontSize":190,"fill":201},"152","✗ basename collisions",[149,208],{"x":209,"y":187,"width":188,"height":189,"rx":190,"fill":210,"stroke":211,"strokeWidth":193},"496","#f7f0da","#f2cc8f",[157,213,214],{"x":173,"y":196,"textAnchor":161,"fontSize":169,"fontWeight":163,"fill":164},"half fixed",[157,216,219],{"x":217,"y":178,"fontSize":190,"fill":218},"514","#8a5a00","✗ still shadows",[157,221,223],{"x":217,"y":205,"fontSize":190,"fill":222},"#2a5f49","✓ no collisions",[149,225],{"x":186,"y":226,"width":188,"height":189,"rx":190,"fill":210,"stroke":211,"strokeWidth":193},"186",[157,228,214],{"x":153,"y":229,"textAnchor":161,"fontSize":169,"fontWeight":163,"fill":164},"212",[157,231,233],{"x":200,"y":232,"fontSize":190,"fill":222},"236","✓ tests the installed copy",[157,235,206],{"x":200,"y":236,"fontSize":190,"fill":218},"258",[149,238],{"x":209,"y":226,"width":188,"height":189,"rx":190,"fill":239,"stroke":240,"strokeWidth":193},"#e6f0ea","#81b29a",[157,242,243],{"x":173,"y":229,"textAnchor":161,"fontSize":169,"fontWeight":163,"fill":164},"recommended",[157,245,233],{"x":217,"y":232,"fontSize":190,"fill":222},[157,247,248],{"x":217,"y":236,"fontSize":190,"fill":222},"✓ no __init__.py needed",[250,251,252],"figcaption",{},"Both problems are real and independent, which is why fixing one and declaring victory leaves half the failures in place.",[25,254,256],{"id":255},"step-by-step-implementation","Step-by-step implementation",[258,259,261],"h3",{"id":260},"_1-move-to-a-src-layout","1. Move to a src layout",[263,264,269],"pre",{"className":265,"code":267,"language":157,"meta":268},[266],"language-text","repository\u002F\n├── pyproject.toml\n├── src\u002F\n│   └── myapp\u002F\n│       ├── __init__.py\n│       └── billing.py\n└── tests\u002F\n    ├── conftest.py\n    ├── billing\u002F\n    │   └── test_invoice.py\n    └── api\u002F\n        └── test_invoice.py      ← same basename, different directory\n","",[13,270,267],{"__ignoreMap":268},[10,272,273,274,277,278,281,282,285],{},"With ",[13,275,276],{},"src\u002F",", the repository root contains no importable ",[13,279,280],{},"myapp",", so ",[13,283,284],{},"import myapp"," can only resolve through the installed distribution. That single property removes an entire class of \"works locally, fails in CI\" failure, because locally and in CI now import the same thing.",[258,287,289],{"id":288},"_2-install-the-package-rather-than-relying-on-path-insertion","2. Install the package rather than relying on path insertion",[263,291,295],{"className":292,"code":293,"language":294,"meta":268,"style":268},"language-bash shiki shiki-themes github-light github-dark","pip install -e .          # editable install: src\u002Fmyapp becomes importable everywhere\n","bash",[13,296,297],{"__ignoreMap":268},[298,299,302,306,310,314,317],"span",{"class":300,"line":301},"line",1,[298,303,305],{"class":304},"sScJk","pip",[298,307,309],{"class":308},"sZZnC"," install",[298,311,313],{"class":312},"sj4cs"," -e",[298,315,316],{"class":308}," .",[298,318,320],{"class":319},"sJ8bj","          # editable install: src\u002Fmyapp becomes importable everywhere\n",[10,322,323,324,326,327,329],{},"An editable install puts the package on ",[13,325,55],{}," through the mechanism packaging provides, rather than through pytest's path manipulation. Tests then import ",[13,328,280],{}," exactly the way a user would.",[258,331,333],{"id":332},"_3-declare-the-configuration-explicitly","3. Declare the configuration explicitly",[263,335,339],{"className":336,"code":337,"language":338,"meta":268,"style":268},"language-toml shiki shiki-themes github-light github-dark","# pyproject.toml\n[tool.pytest.ini_options]\ntestpaths = [\"tests\"]\n# importlib: no sys.path insertion, no module-name collisions, no __init__.py.\naddopts = \"--import-mode=importlib\"\n","toml",[13,340,341,346,352,358,364],{"__ignoreMap":268},[298,342,343],{"class":300,"line":301},[298,344,345],{},"# pyproject.toml\n",[298,347,349],{"class":300,"line":348},2,[298,350,351],{},"[tool.pytest.ini_options]\n",[298,353,355],{"class":300,"line":354},3,[298,356,357],{},"testpaths = [\"tests\"]\n",[298,359,361],{"class":300,"line":360},4,[298,362,363],{},"# importlib: no sys.path insertion, no module-name collisions, no __init__.py.\n",[298,365,367],{"class":300,"line":366},5,[298,368,369],{},"addopts = \"--import-mode=importlib\"\n",[10,371,372,375],{},[13,373,374],{},"testpaths"," stops collection depending on the directory the command was run from. The import mode set in configuration rather than on the command line means every invocation — local, CI, an IDE's test runner — behaves the same.",[258,377,379,380,383],{"id":378},"_4-delete-the-__init__py-files-from-the-test-tree","4. Delete the ",[13,381,382],{},"__init__.py"," files from the test tree",[10,385,386,387,389,390,392,393,395],{},"Under ",[13,388,120],{}," they are unnecessary, and leaving them creates a package whose name can collide with a real one. Under ",[13,391,112],{}," they are mandatory wherever two test files share a basename, which is why suites that stay on ",[13,394,112],{}," end up with them everywhere.",[258,397,399],{"id":398},"_5-verify-from-more-than-one-directory","5. Verify from more than one directory",[263,401,403],{"className":292,"code":402,"language":294,"meta":268,"style":268},"pytest -q                       # from the repository root\n(cd tests\u002Fbilling && pytest -q) # from a subdirectory\npython -c \"import myapp; print(myapp.__file__)\"\n",[13,404,405,416,441],{"__ignoreMap":268},[298,406,407,410,413],{"class":300,"line":301},[298,408,409],{"class":304},"pytest",[298,411,412],{"class":312}," -q",[298,414,415],{"class":319},"                       # from the repository root\n",[298,417,418,422,425,428,431,433,435,438],{"class":300,"line":348},[298,419,421],{"class":420},"sVt8B","(",[298,423,424],{"class":312},"cd",[298,426,427],{"class":308}," tests\u002Fbilling",[298,429,430],{"class":420}," && ",[298,432,409],{"class":304},[298,434,412],{"class":312},[298,436,437],{"class":420},") ",[298,439,440],{"class":319},"# from a subdirectory\n",[298,442,443,446,449],{"class":300,"line":354},[298,444,445],{"class":304},"python",[298,447,448],{"class":312}," -c",[298,450,451],{"class":308}," \"import myapp; print(myapp.__file__)\"\n",[263,453,456],{"className":454,"code":455,"language":157,"meta":268},[266],"\u002Fhome\u002Fdev\u002Fproject\u002F.venv\u002Flib\u002Fpython3.12\u002Fsite-packages\u002Fmyapp\u002F__init__.py\n",[13,457,455],{"__ignoreMap":268},[10,459,460],{},"The last command is the check that matters. If it prints a path inside the working tree rather than inside the environment, something is shadowing the installed package and the suite is not testing what will ship.",[25,462,464],{"id":463},"verification","Verification",[10,466,467],{},"Two properties are worth asserting continuously rather than checking once.",[263,469,472],{"className":470,"code":471,"language":445,"meta":268,"style":268},"language-python shiki shiki-themes github-light github-dark","# tests\u002Ftest_packaging.py\nimport pathlib\nimport sysconfig\n\nimport myapp\n\n\ndef test_imports_the_installed_distribution():\n    \"\"\"Guards against a path insertion shadowing the installed package.\"\"\"\n    site_packages = pathlib.Path(sysconfig.get_paths()[\"purelib\"]).resolve()\n    module_path = pathlib.Path(myapp.__file__).resolve()\n    assert site_packages in module_path.parents, f\"imported {module_path}\"\n",[13,473,474,479,484,489,495,500,505,510,516,522,528,534],{"__ignoreMap":268},[298,475,476],{"class":300,"line":301},[298,477,478],{},"# tests\u002Ftest_packaging.py\n",[298,480,481],{"class":300,"line":348},[298,482,483],{},"import pathlib\n",[298,485,486],{"class":300,"line":354},[298,487,488],{},"import sysconfig\n",[298,490,491],{"class":300,"line":360},[298,492,494],{"emptyLinePlaceholder":493},true,"\n",[298,496,497],{"class":300,"line":366},[298,498,499],{},"import myapp\n",[298,501,503],{"class":300,"line":502},6,[298,504,494],{"emptyLinePlaceholder":493},[298,506,508],{"class":300,"line":507},7,[298,509,494],{"emptyLinePlaceholder":493},[298,511,513],{"class":300,"line":512},8,[298,514,515],{},"def test_imports_the_installed_distribution():\n",[298,517,519],{"class":300,"line":518},9,[298,520,521],{},"    \"\"\"Guards against a path insertion shadowing the installed package.\"\"\"\n",[298,523,525],{"class":300,"line":524},10,[298,526,527],{},"    site_packages = pathlib.Path(sysconfig.get_paths()[\"purelib\"]).resolve()\n",[298,529,531],{"class":300,"line":530},11,[298,532,533],{},"    module_path = pathlib.Path(myapp.__file__).resolve()\n",[298,535,537],{"class":300,"line":536},12,[298,538,539],{},"    assert site_packages in module_path.parents, f\"imported {module_path}\"\n",[263,541,543],{"className":292,"code":542,"language":294,"meta":268,"style":268},"# Collection must be identical from anywhere in the tree.\npytest --collect-only -q | md5sum\n(cd tests && pytest --collect-only -q | md5sum)\n",[13,544,545,550,566],{"__ignoreMap":268},[298,546,547],{"class":300,"line":301},[298,548,549],{"class":319},"# Collection must be identical from anywhere in the tree.\n",[298,551,552,554,557,559,563],{"class":300,"line":348},[298,553,409],{"class":304},[298,555,556],{"class":312}," --collect-only",[298,558,412],{"class":312},[298,560,562],{"class":561},"szBVR"," |",[298,564,565],{"class":304}," md5sum\n",[298,567,568,570,572,575,577,579,581,583,585,588],{"class":300,"line":354},[298,569,421],{"class":420},[298,571,424],{"class":312},[298,573,574],{"class":308}," tests",[298,576,430],{"class":420},[298,578,409],{"class":304},[298,580,556],{"class":312},[298,582,412],{"class":312},[298,584,562],{"class":561},[298,586,587],{"class":304}," md5sum",[298,589,590],{"class":420},")\n",[10,592,593,594,596],{},"Matching checksums mean ",[13,595,374],{}," and the import mode are doing their job. Differing ones mean collection depends on the working directory, and somebody's IDE will eventually run a different set of tests from CI.",[25,598,600],{"id":599},"troubleshooting","Troubleshooting",[602,603,604,620],"table",{},[605,606,607],"thead",{},[608,609,610,614,617],"tr",{},[611,612,613],"th",{},"Symptom",[611,615,616],{},"Root cause",[611,618,619],{},"Fix",[621,622,623,641,656,670,685,699],"tbody",{},[608,624,625,630,633],{},[626,627,628],"td",{},[13,629,19],{},[626,631,632],{},"Two test files share a basename in non-package directories",[626,634,635,638,639],{},[13,636,637],{},"--import-mode=importlib",", or add ",[13,640,382],{},[608,642,643,648,651],{},[626,644,645],{},[13,646,647],{},"ModuleNotFoundError: myapp",[626,649,650],{},"Package never installed; relying on rootdir insertion",[626,652,653],{},[13,654,655],{},"pip install -e .",[608,657,658,661,664],{},[626,659,660],{},"Local pass, CI fail on a changed module",[626,662,663],{},"Flat layout shadowing the installed copy",[626,665,666,667,669],{},"Move to a ",[13,668,95],{}," layout",[608,671,672,676,682],{},[626,673,674],{},[13,675,15],{},[626,677,678,681],{},[13,679,680],{},"conftest.py"," imports something only on the local path",[626,683,684],{},"Import through the installed package",[608,686,687,690,693],{},[626,688,689],{},"IDE runs a different test set",[626,691,692],{},"Collection rooted at a different directory",[626,694,695,696,698],{},"Set ",[13,697,374],{}," in configuration",[608,700,701,706,709],{},[626,702,703],{},[13,704,705],{},"attempted relative import with no known parent package",[626,707,708],{},"Test file imported as a top-level module",[626,710,711],{},"Use absolute imports in tests",[25,713,715],{"id":714},"what-prepend-mode-actually-does","What prepend mode actually does",[10,717,718,719,721,722,725,726,728],{},"Understanding the default explains almost every mismatch error. For each test file, pytest walks upward from the file's directory for as long as it finds ",[13,720,382],{},", and stops at the first directory that has none. That directory is the ",[81,723,724],{},"basedir",". pytest inserts the basedir at ",[13,727,116],{}," and imports the file under the dotted name formed from its path relative to the basedir.",[10,730,731,732,735,736,738,739,742,743,746,747,750,751,753,754,757],{},"So ",[13,733,734],{},"tests\u002Fbilling\u002Ftest_invoice.py"," with no ",[13,737,382],{}," anywhere becomes module ",[13,740,741],{},"test_invoice",", with ",[13,744,745],{},"tests\u002Fbilling"," on the path. And ",[13,748,749],{},"tests\u002Fapi\u002Ftest_invoice.py"," becomes module ",[13,752,741],{}," as well — the same name, already in ",[13,755,756],{},"sys.modules",", pointing at a different file. pytest detects the mismatch and raises rather than silently running the wrong file, which is the error everyone has seen.",[10,759,760,761,763,764,16,766,769,770,773,774,769,777,780,781,784,785,787],{},"Adding ",[13,762,382],{}," to ",[13,765,91],{},[13,767,768],{},"tests\u002Fbilling\u002F"," and ",[13,771,772],{},"tests\u002Fapi\u002F"," changes the basedir to the repository root and the module names to ",[13,775,776],{},"tests.billing.test_invoice",[13,778,779],{},"tests.api.test_invoice",", which are distinct. That works, and it is why the convention exists. The cost is that the test tree is now a package whose name is ",[13,782,783],{},"tests",", which collides with any other installed distribution named ",[13,786,783],{}," and which must be excluded from the built wheel.",[10,789,790,792,793,795,796,799,800,802],{},[13,791,120],{}," mode sidesteps the whole mechanism: the file is imported by location, given a unique name, and nothing is added to ",[13,794,55],{},". The historic reason not to use it was that test modules could not import each other by name; since pytest 8 the ",[13,797,798],{},"consider_namespace_packages"," option and improved name resolution make that rare enough that ",[13,801,120],{}," is the better default for a new project.",[126,804,806,900],{"className":805},[129],[131,807,139,812,139,815,139,818,139,821,139,824,139,832,139,837,139,840,139,844,139,847,139,850,139,854,139,858,139,861,139,864,139,868,139,870,139,872,139,875,139,878,139,881,139,884,139,887,139,890,139,895,139,897],{"viewBox":808,"role":134,"ariaLabelledBy":809,"xmlns":138},"0 0 840 276",[810,811],"prep-t","prep-d",[141,813,814],{"id":810},"How prepend mode derives a module name, and where it collides",[145,816,817],{"id":811},"Two test files with the same basename in sibling directories both resolve to the module name test_invoice under prepend mode, colliding in sys.modules. Adding init files moves the basedir to the repository root so the names become distinct dotted paths, while importlib mode gives each file a unique name without touching sys.path.",[149,819],{"x":151,"y":151,"width":152,"height":820,"rx":154,"fill":155},"276",[157,822,823],{"x":159,"y":160,"textAnchor":161,"fontSize":162,"fontWeight":163,"fill":164},"The same basename, two different resolutions",[149,825],{"x":826,"y":827,"width":828,"height":829,"rx":190,"fill":830,"stroke":164,"strokeWidth":831},"26","52","250","88","#f4f1de","1.6",[157,833,734],{"x":834,"y":835,"textAnchor":161,"fontSize":836,"fontWeight":163,"fill":164},"151","76","11.5",[157,838,749],{"x":834,"y":839,"textAnchor":161,"fontSize":836,"fontWeight":163,"fill":164},"98",[157,841,843],{"x":834,"y":842,"textAnchor":161,"fontSize":190,"fill":164},"122","no __init__.py anywhere",[149,845],{"x":826,"y":846,"width":828,"height":177,"rx":190,"fill":191,"stroke":192,"strokeWidth":193},"156",[157,848,112],{"x":834,"y":849,"textAnchor":161,"fontSize":169,"fontWeight":163,"fill":164},"180",[157,851,853],{"x":834,"y":852,"textAnchor":161,"fontSize":190,"fill":164},"202","both → \"test_invoice\"",[157,855,857],{"x":834,"y":856,"textAnchor":161,"fontSize":190,"fill":201},"222","sys.modules collision",[157,859,19],{"x":834,"y":860,"textAnchor":161,"fontSize":190,"fill":201},"242",[149,862],{"x":863,"y":846,"width":828,"height":177,"rx":190,"fill":210,"stroke":211,"strokeWidth":193},"296",[157,865,867],{"x":866,"y":849,"textAnchor":161,"fontSize":169,"fontWeight":163,"fill":164},"421","prepend + __init__.py",[157,869,776],{"x":866,"y":852,"textAnchor":161,"fontSize":190,"fill":164},[157,871,779],{"x":866,"y":856,"textAnchor":161,"fontSize":190,"fill":164},[157,873,874],{"x":866,"y":860,"textAnchor":161,"fontSize":190,"fill":218},"works; \"tests\" is now a package",[149,876],{"x":877,"y":846,"width":188,"height":177,"rx":190,"fill":239,"stroke":240,"strokeWidth":193},"566",[157,879,120],{"x":880,"y":849,"textAnchor":161,"fontSize":169,"fontWeight":163,"fill":164},"690",[157,882,883],{"x":880,"y":852,"textAnchor":161,"fontSize":190,"fill":164},"imported by location",[157,885,886],{"x":880,"y":856,"textAnchor":161,"fontSize":190,"fill":222},"unique names, no sys.path edit",[157,888,889],{"x":880,"y":860,"textAnchor":161,"fontSize":190,"fill":222},"no __init__.py needed",[300,891],{"x1":834,"y1":892,"x2":834,"y2":205,"stroke":893,"strokeWidth":894},"144","rgba(61,64,91,0.4)","1.4",[300,896],{"x1":820,"y1":177,"x2":866,"y2":205,"stroke":893,"strokeWidth":894},[300,898],{"x1":820,"y1":899,"x2":880,"y2":205,"stroke":893,"strokeWidth":894},"110",[250,901,902],{},"All three columns can be made to work. Only the right-hand one requires no packaging decisions about the test tree itself.",[25,904,906],{"id":905},"where-conftestpy-is-found-and-where-it-is-not","Where conftest.py is found, and where it is not",[10,908,909,911],{},[13,910,680],{}," follows its own rules, separate from the import mode, and they explain a second family of errors.",[10,913,914,915,917,918,920,921,924,925,928,929,931],{},"pytest collects ",[13,916,680],{}," files from the rootdir down to each collected test file's directory, and applies them in that order: the root one first, the nearest one last. A fixture defined in a nearer file overrides one of the same name defined further up. Crucially, a ",[13,919,680],{}," in a ",[81,922,923],{},"sibling"," directory is never consulted — ",[13,926,927],{},"tests\u002Fapi\u002Fconftest.py"," has no effect on ",[13,930,768],{},", however much it looks like shared configuration.",[126,933,935,1034],{"className":934},[129],[131,936,139,941,139,944,139,947,139,964,139,968,139,972,139,977,139,981,139,984,139,989,139,994,139,998,139,1001,139,1004,139,1009,139,1013,139,1019,139,1023,139,1030],{"viewBox":937,"role":134,"ariaLabelledBy":938,"xmlns":138},"0 0 800 268",[939,940],"cft-t","cft-d",[141,942,943],{"id":939},"Which conftest files apply to a given test",[145,945,946],{"id":940},"A directory tree. For a test in tests slash billing slash test invoice, the root conftest and the tests slash billing conftest both apply in that order, while the sibling tests slash api conftest does not apply at all. Fixtures in the nearer file override same-named fixtures in the outer one.",[948,949,950,951,139],"defs",{},"\n    ",[952,953,960],"marker",{"id":954,"viewBox":955,"refX":956,"refY":957,"markerWidth":958,"markerHeight":958,"orient":959},"cft-a","0 0 10 10","9","5","7","auto-start-reverse",[961,962],"path",{"d":963,"fill":240},"M0 0 L10 5 L0 10 z",[149,965],{"x":151,"y":151,"width":966,"height":967,"rx":154,"fill":155},"800","268",[157,969,971],{"x":970,"y":160,"textAnchor":161,"fontSize":162,"fontWeight":163,"fill":164},"400","Applied from the root down, never sideways",[149,973],{"x":974,"y":827,"width":153,"height":975,"rx":976,"fill":239,"stroke":240,"strokeWidth":193},"34","44","10",[157,978,980],{"x":979,"y":187,"textAnchor":161,"fontSize":836,"fontWeight":163,"fill":164},"184","tests\u002Fconftest.py — applies",[149,982],{"x":983,"y":899,"width":153,"height":975,"rx":976,"fill":239,"stroke":240,"strokeWidth":193},"70",[157,985,988],{"x":986,"y":987,"textAnchor":161,"fontSize":836,"fontWeight":163,"fill":164},"220","138","tests\u002Fbilling\u002Fconftest.py — applies",[149,990],{"x":196,"y":991,"width":992,"height":975,"rx":976,"fill":830,"stroke":164,"strokeWidth":993},"168","330","1.8",[157,995,734],{"x":996,"y":997,"textAnchor":161,"fontSize":836,"fontWeight":163,"fill":164},"271","196",[149,999],{"x":1000,"y":899,"width":153,"height":975,"rx":976,"fill":191,"stroke":192,"strokeWidth":193},"470",[157,1002,1003],{"x":173,"y":987,"textAnchor":161,"fontSize":836,"fontWeight":163,"fill":164},"tests\u002Fapi\u002Fconftest.py — ignored here",[300,1005],{"x1":979,"y1":1006,"x2":1007,"y2":196,"stroke":240,"strokeWidth":993,"markerEnd":1008},"100","200","url(#cft-a)",[300,1010],{"x1":986,"y1":1011,"x2":828,"y2":1012,"stroke":240,"strokeWidth":993,"markerEnd":1008},"158","164",[300,1014],{"x1":1000,"y1":1015,"x2":1016,"y2":1015,"stroke":192,"strokeWidth":993,"strokeDashArray":1017},"132","380",[957,1018],"4",[157,1020,1022],{"x":1021,"y":842,"textAnchor":161,"fontSize":190,"fill":201},"424","✗",[149,1024],{"x":974,"y":1025,"width":1026,"height":1027,"rx":956,"fill":155,"stroke":1028,"strokeWidth":1029},"226","736","32","rgba(61,64,91,0.35)","1.5",[157,1031,1033],{"x":970,"y":1032,"textAnchor":161,"fontSize":836,"fill":164},"247","A fixture needed by two sibling directories belongs in their common parent, not duplicated in both.",[250,1035,1036],{},"Every \"fixture not found\" that survives an import-mode fix is this rule: the fixture is defined in a directory that is not an ancestor of the test requesting it.",[10,1038,1039,1040,1042,1043,1046,1047,1050,1051,1053,1054,1056,1057,1059,1060,1063],{},"One further rule catches people out. ",[13,1041,680],{}," files are imported ",[81,1044,1045],{},"before"," collection of the directory they govern, and they are always rewritten for assertions, but they are not affected by ",[13,1048,1049],{},"--import-mode"," in the same way test modules are — pytest imports them by path under a unique name regardless. That is why a ",[13,1052,680],{}," can safely exist in every directory without ",[13,1055,382],{},", even under ",[13,1058,112],{},", while two ",[13,1061,1062],{},"test_invoice.py"," files cannot.",[25,1065,1067],{"id":1066},"organising-the-tree-once-imports-are-settled","Organising the tree once imports are settled",[10,1069,1070],{},"With imports out of the way, the layout question becomes purely about navigation, and two conventions do most of the work.",[10,1072,1073,1076,1077,1079,1080,1083],{},[76,1074,1075],{},"Mirror the package."," ",[13,1078,734],{}," for ",[13,1081,1082],{},"src\u002Fmyapp\u002Fbilling\u002Finvoice.py"," means a reader can find a module's tests without searching, and a reviewer can see at a glance that a changed module has no test changes. It also makes coverage gaps visible as missing directories.",[10,1085,1086,1089],{},[76,1087,1088],{},"Separate by cost, not by philosophy."," A top-level split between fast tests and those needing a database or a network is worth having, because it is the split CI uses. Splitting by \"unit\" and \"integration\" as categories tends to produce arguments about which a given test is; splitting by \"needs Docker\" and \"does not\" produces none, because it is a fact about the test rather than an opinion.",[263,1091,1094],{"className":1092,"code":1093,"language":157,"meta":268},[266],"tests\u002F\n├── conftest.py                 # shared, cheap fixtures only\n├── unit\u002F                       # no I\u002FO; runs on every push\n│   └── billing\u002Ftest_invoice.py\n└── integration\u002F                # needs the container stack\n    ├── conftest.py             # database and service fixtures\n    └── billing\u002Ftest_invoice.py\n",[13,1095,1093],{"__ignoreMap":268},[10,1097,1098,1099,1102,1103,1106,1107,1110,1111,1114,1115,65],{},"Placing the expensive fixtures in ",[13,1100,1101],{},"tests\u002Fintegration\u002Fconftest.py"," rather than the root one matters more than it looks: a session-scoped container fixture defined at the root is ",[81,1104,1105],{},"collected"," for every run, and an accidental ",[13,1108,1109],{},"autouse"," or an import at module level will start Docker even for ",[13,1112,1113],{},"pytest tests\u002Funit",". Keeping it in the subtree that needs it means the fast suite never touches it, a property covered further in ",[61,1116,1118],{"href":1117},"\u002Fadvanced-pytest-architecture-configuration\u002Fmanaging-conftest-hierarchies\u002F","managing conftest hierarchies",[25,1120,1122],{"id":1121},"monorepos-and-multiple-distributions","Monorepos and multiple distributions",[10,1124,1125],{},"A repository containing several installable packages multiplies every decision above, and the failure mode is subtle: one package's tests silently importing another package's working copy instead of its installed version.",[263,1127,1130],{"className":1128,"code":1129,"language":157,"meta":268},[266],"repository\u002F\n├── pyproject.toml              # workspace root: dev tooling only\n├── packages\u002F\n│   ├── billing\u002F\n│   │   ├── pyproject.toml\n│   │   ├── src\u002Fbilling\u002F\n│   │   └── tests\u002F\n│   └── notifications\u002F\n│       ├── pyproject.toml\n│       ├── src\u002Fnotifications\u002F\n│       └── tests\u002F\n└── pytest.ini                  # rootdir anchor for whole-repo runs\n",[13,1131,1129],{"__ignoreMap":268},[10,1133,1134,1135,1137,1138,1140,1141,1144,1145,1148],{},"Each package keeps its own ",[13,1136,95],{}," layout and its own tests, and every package is installed editable into the shared environment. A whole-repository run then collects every ",[13,1139,91],{}," directory, and each test imports through the installed distributions rather than through path insertion — which is what makes a change to ",[13,1142,1143],{},"billing"," correctly break ",[13,1146,1147],{},"notifications","' tests when it should.",[10,1150,1151,1152,1155,1156,1158,1159,1161],{},"Two configuration details keep this working. The root configuration file sets ",[13,1153,1154],{},"testpaths = [\"packages\"]"," so that running ",[13,1157,409],{}," from the root behaves the same as running it from a package. And each package's ",[13,1160,48],{}," declares its dependency on the others by name, so the environment resolves the graph rather than relying on everything happening to be on the path.",[10,1163,1164,1165,1167,1168,65],{},"The temptation to avoid is a single ",[13,1166,680],{}," at the repository root providing fixtures for every package. It creates a dependency from each package's tests to the root of the monorepo, which means a package can no longer be tested in isolation — exactly the property a monorepo is supposed to preserve. Shared fixtures belong in a small installed package of their own, imported explicitly by whoever needs them, as described in ",[61,1169,1171],{"href":1170},"\u002Fadvanced-pytest-architecture-configuration\u002Fmanaging-conftest-hierarchies\u002Fsharing-fixtures-without-conftest-py\u002F","sharing fixtures without conftest.py",[25,1173,1175],{"id":1174},"testing-the-installed-artefact-not-the-working-copy","Testing the installed artefact, not the working copy",[10,1177,1178],{},"The strongest argument for the whole arrangement is that it makes one specific test possible: running the suite against the built wheel rather than the source tree.",[263,1180,1182],{"className":292,"code":1181,"language":294,"meta":268,"style":268},"python -m build                             # produces dist\u002Fmyapp-1.4.0-py3-none-any.whl\npython -m venv \u002Ftmp\u002Fverify && \u002Ftmp\u002Fverify\u002Fbin\u002Fpip install dist\u002F*.whl pytest\ncd \u002Ftmp && \u002Ftmp\u002Fverify\u002Fbin\u002Fpytest --import-mode=importlib \u002Fpath\u002Fto\u002Frepo\u002Ftests\n",[13,1183,1184,1197,1228],{"__ignoreMap":268},[298,1185,1186,1188,1191,1194],{"class":300,"line":301},[298,1187,445],{"class":304},[298,1189,1190],{"class":312}," -m",[298,1192,1193],{"class":308}," build",[298,1195,1196],{"class":319},"                             # produces dist\u002Fmyapp-1.4.0-py3-none-any.whl\n",[298,1198,1199,1201,1203,1206,1209,1211,1214,1216,1219,1222,1225],{"class":300,"line":348},[298,1200,445],{"class":304},[298,1202,1190],{"class":312},[298,1204,1205],{"class":308}," venv",[298,1207,1208],{"class":308}," \u002Ftmp\u002Fverify",[298,1210,430],{"class":420},[298,1212,1213],{"class":304},"\u002Ftmp\u002Fverify\u002Fbin\u002Fpip",[298,1215,309],{"class":308},[298,1217,1218],{"class":308}," dist\u002F",[298,1220,1221],{"class":312},"*",[298,1223,1224],{"class":308},".whl",[298,1226,1227],{"class":308}," pytest\n",[298,1229,1230,1232,1235,1237,1240,1243],{"class":300,"line":354},[298,1231,424],{"class":312},[298,1233,1234],{"class":308}," \u002Ftmp",[298,1236,430],{"class":420},[298,1238,1239],{"class":304},"\u002Ftmp\u002Fverify\u002Fbin\u002Fpytest",[298,1241,1242],{"class":312}," --import-mode=importlib",[298,1244,1245],{"class":308}," \u002Fpath\u002Fto\u002Frepo\u002Ftests\n",[10,1247,1248,1249,1252,1253,1255,1256,1258,1259,1261],{},"Running from ",[13,1250,1251],{},"\u002Ftmp"," guarantees the working tree is nowhere near ",[13,1254,55],{},", so the only importable ",[13,1257,280],{}," is the one inside the wheel. This catches the packaging mistakes that no amount of source-tree testing can: a subpackage missing from the wheel because it had no ",[13,1260,382],{},", a data file not declared in the package data, a module that imported successfully only because a sibling directory happened to be adjacent.",[10,1263,1264],{},"It is worth running on every release and cheap enough to run nightly. Teams that ship libraries tend to add it after the first bug report that says \"it works when I clone the repository but not when I pip install it\", which is always a packaging problem and never a code one.",[25,1266,1268],{"id":1267},"migrating-an-existing-suite","Migrating an existing suite",[10,1270,1271],{},"Changing layout and import mode in a live repository is a mechanical change with one sharp edge, so the order matters.",[10,1273,1274,1275,1277,1278,1280,1281,1284],{},"Move to the ",[13,1276,95],{}," layout first, in its own change, with ",[13,1279,655],{}," added to the contributing instructions and to every CI job. This is the step most likely to break something — a script that did ",[13,1282,1283],{},"from myapp import x"," relying on the working directory, a tool configured with a relative path — and isolating it makes the breakage attributable.",[10,1286,1287,1288,1290,1291,1294,1295,1297],{},"Switch the import mode second. Add ",[13,1289,637],{},", run the suite, and expect failures only where test modules import each other by bare module name (",[13,1292,1293],{},"from test_helpers import make_order","). Those should become imports from a proper helper package under ",[13,1296,91],{}," that is installed or path-configured deliberately, which is a better arrangement anyway.",[10,1299,1300,1301,1303,1304,65],{},"Delete the ",[13,1302,382],{}," files last, once the suite is green under the new mode, so that reverting the mode change remains possible up to that point. Doing all three at once produces a change nobody can bisect, and these failures are exactly the kind where bisecting is the fastest route to the cause. The specific errors and their fixes are catalogued in ",[61,1305,1307],{"href":1306},"\u002Fadvanced-pytest-architecture-configuration\u002Ftest-layout-and-import-modes\u002Ffixing-import-file-mismatch-errors-in-pytest\u002F","fixing import file mismatch errors in pytest",[25,1309,1311],{"id":1310},"a-checklist-that-prevents-recurrence","A checklist that prevents recurrence",[10,1313,1314],{},"Import problems come back whenever a repository changes shape, so the durable fix is a handful of assertions about the project rather than a one-off cleanup.",[10,1316,1317,1318,1320,1321,769,1323,1325,1326,1328,1329,1331],{},"The package is installed, not merely present: ",[13,1319,655],{}," in the contributing instructions and in every CI job, with the packaging test above guarding it. The configuration is explicit: ",[13,1322,374],{},[13,1324,1049],{}," live in ",[13,1327,48],{},", never in a shell alias or an IDE setting where only one person has them. The test tree is not a distribution: no ",[13,1330,382],{},", excluded from the wheel, and containing nothing another package imports. And collection is location-independent, verified by the two-directory checksum comparison.",[10,1333,1334],{},"Four properties, each checkable in seconds, and between them they cover every failure in the troubleshooting table above. Writing them into a short note beside the configuration is worth more than fixing the errors individually, because the next person to add a package or move a directory reads the note instead of rediscovering the rules from the error messages.",[25,1336,1338],{"id":1337},"frequently-asked-questions","Frequently Asked Questions",[10,1340,1341,1344,1345,1347,1348,1350,1351,1353,1354,1356],{},[76,1342,1343],{},"What actually causes 'import file mismatch' in pytest?","\nTwo test files with the same basename in directories that are not packages. Under the default ",[13,1346,112],{}," import mode pytest derives a module name from the basename alone, so the second file collides with the first in ",[13,1349,756],{},". Adding ",[13,1352,382],{}," files makes the names unique, and ",[13,1355,41],{}," removes the collision entirely.",[10,1358,1359,1362,1363,1365,1366,1368],{},[76,1360,1361],{},"Should tests live inside the package or beside it?","\nBeside it, in a top-level ",[13,1364,783],{}," directory, when the package uses a ",[13,1367,95],{}," layout. Tests inside the package ship to users, get imported by anything that imports the package, and make it impossible to test the installed distribution rather than the working copy. Ship tests inside the package only when downstream consumers are meant to run them.",[10,1370,1371,1378,1379,1381,1382,1384,1385,1387],{},[76,1372,1373,1374,1377],{},"Do test directories still need ",[76,1375,1376],{},"init",".py files?","\nNot with ",[13,1380,41],{},", which is the reason to switch. Under the default ",[13,1383,112],{}," mode they are needed wherever two test files could share a basename, which in a suite of any size is everywhere. Choosing ",[13,1386,120],{}," and deleting them is usually the simpler answer.",[10,1389,1390,1393,1394,1396,1397,1399],{},[76,1391,1392],{},"Why does pytest import a different copy of my package than I expect?","\nBecause rootdir insertion put the working directory on ",[13,1395,55],{}," ahead of site-packages. With a flat layout the local directory shadows the installed distribution, so you test the working copy even when you meant to test the wheel. A ",[13,1398,95],{}," layout makes this impossible, which is its main argument.",[10,1401,1402,1405,1406,1408],{},[76,1403,1404],{},"What is rootdir used for, exactly?","\nIt anchors relative paths in configuration, cache locations and node identifiers. It does not by itself control imports. Import behaviour comes from the import mode and from which directories pytest inserts into ",[13,1407,55],{},", which are related to rootdir but not the same thing.",[25,1410,1412],{"id":1411},"related-guides","Related guides",[30,1414,1415,1420,1430,1435,1442],{},[33,1416,1417,1418,65],{},"Diagnose the specific error in ",[61,1419,1307],{"href":1306},[33,1421,1422,1423,65],{},"Make the switch deliberately with ",[61,1424,1426,1427,1429],{"href":1425},"\u002Fadvanced-pytest-architecture-configuration\u002Ftest-layout-and-import-modes\u002Fusing-importmode-importlib-without-init-files\u002F","using importmode=importlib without ",[76,1428,1376],{},".py files",[33,1431,1432,1433,65],{},"Keep expensive fixtures out of the fast suite using ",[61,1434,1118],{"href":1117},[33,1436,1437,1438,65],{},"Cut collection cost once the tree is settled with ",[61,1439,1441],{"href":1440},"\u002Fadvanced-pytest-architecture-configuration\u002Foptimizing-test-discovery\u002F","optimizing test discovery",[33,1443,1444,1445,65],{},"Put the configuration in the right file per ",[61,1446,64],{"href":63},[10,1448,1449,1450],{},"← Back to ",[61,1451,1453],{"href":1452},"\u002Fadvanced-pytest-architecture-configuration\u002F","Advanced Pytest Architecture & Configuration",[1455,1456,1457],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":268,"searchDepth":348,"depth":348,"links":1459},[1460,1461,1462,1470,1471,1472,1473,1474,1475,1476,1477,1478,1479,1480],{"id":27,"depth":348,"text":28},{"id":68,"depth":348,"text":69},{"id":255,"depth":348,"text":256,"children":1463},[1464,1465,1466,1467,1469],{"id":260,"depth":354,"text":261},{"id":288,"depth":354,"text":289},{"id":332,"depth":354,"text":333},{"id":378,"depth":354,"text":1468},"4. Delete the __init__.py files from the test tree",{"id":398,"depth":354,"text":399},{"id":463,"depth":348,"text":464},{"id":599,"depth":348,"text":600},{"id":714,"depth":348,"text":715},{"id":905,"depth":348,"text":906},{"id":1066,"depth":348,"text":1067},{"id":1121,"depth":348,"text":1122},{"id":1174,"depth":348,"text":1175},{"id":1267,"depth":348,"text":1268},{"id":1310,"depth":348,"text":1311},{"id":1337,"depth":348,"text":1338},{"id":1411,"depth":348,"text":1412},"Fix import errors for good: src vs flat layout, rootdir and sys.path insertion, prepend vs importlib import modes, __init__.py rules, and duplicate basename collisions.","md",{"slug":1484,"type":1485,"breadcrumb":1486,"datePublished":1487,"dateModified":1487,"faq":1488,"howto":1500},"test-layout-and-import-modes","topic","Layout & Imports","2026-09-18",[1489,1491,1493,1496,1498],{"q":1343,"a":1490},"Two test files with the same basename in directories that are not packages. Under the default prepend import mode pytest derives a module name from the basename alone, so the second file collides with the first in sys.modules. Adding __init__.py files makes the names unique, and importmode=importlib removes the collision entirely.",{"q":1361,"a":1492},"Beside it, in a top-level tests directory, when the package uses a src layout. Tests inside the package ship to users, get imported by anything that imports the package, and make it impossible to test the installed distribution rather than the working copy. Ship tests inside the package only when downstream consumers are meant to run them.",{"q":1494,"a":1495},"Do test directories still need __init__.py files?","Not with importmode=importlib, which is the reason to switch. Under the default prepend mode they are needed wherever two test files could share a basename, which in a suite of any size is everywhere. Choosing importlib and deleting them is usually the simpler answer.",{"q":1392,"a":1497},"Because rootdir insertion put the working directory on sys.path ahead of site-packages. With a flat layout the local directory shadows the installed distribution, so you test the working copy even when you meant to test the wheel. A src layout makes this impossible, which is its main argument.",{"q":1404,"a":1499},"It anchors relative paths in configuration, cache locations and node identifiers. It does not by itself control imports. Import behaviour comes from the import mode and from which directories pytest inserts into sys.path, which are related to rootdir but not the same thing.",{"name":1501,"description":1502,"steps":1503},"How to fix and prevent pytest import problems","Choose a layout, set the import mode explicitly, and make the test tree's packaging match the choice.",[1504,1507,1510,1513,1516],{"name":1505,"text":1506},"Adopt a src layout","Move the package under src\u002F so the working directory can never shadow the installed distribution on sys.path.",{"name":1508,"text":1509},"Declare testpaths and the import mode","Set testpaths and importmode=importlib in the pytest configuration so behaviour does not depend on the invocation directory.",{"name":1511,"text":1512},"Remove or add __init__.py deliberately","With importlib, delete them from the test tree; with prepend, add them everywhere two basenames could collide.",{"name":1514,"text":1515},"Install the package under test","Install with pip install -e . so imports resolve through the distribution rather than through a path insertion.",{"name":1517,"text":1518},"Verify from two directories","Run the suite from the repository root and from a subdirectory and confirm identical collection.","\u002Fadvanced-pytest-architecture-configuration\u002Ftest-layout-and-import-modes",{"title":5,"description":1481},"advanced-pytest-architecture-configuration\u002Ftest-layout-and-import-modes\u002Findex","UEQBCkSPz7Q-5gCJxso8VwoxwjjrP5AQ-_cxEk2EEH4",1789718765720]