[{"data":1,"prerenderedAt":1001},["ShallowReactive",2],{"page-\u002Fadvanced-pytest-architecture-configuration\u002Fmanaging-conftest-hierarchies\u002Fsharing-fixtures-without-conftest-py\u002F":3},{"id":4,"title":5,"body":6,"description":967,"extension":968,"meta":969,"navigation":85,"path":997,"seo":998,"stem":999,"__hash__":1000},"content\u002Fadvanced-pytest-architecture-configuration\u002Fmanaging-conftest-hierarchies\u002Fsharing-fixtures-without-conftest-py\u002Findex.md","Sharing pytest Fixtures Without conftest.py",{"type":7,"value":8,"toc":957},"minimark",[9,18,23,47,51,57,158,168,198,209,220,242,245,355,359,375,509,513,516,523,545,559,606,612,616,619,629,649,663,678,684,687,808,812,867,874,884,888,894,909,915,919,947,953],[10,11,12,13,17],"p",{},"A root ",[14,15,16],"code",{},"conftest.py"," starts as three shared fixtures and ends as four hundred lines imported by every test in the repository. Every run pays for every import in it, unrelated suites inherit fixtures they never asked for, and the file becomes a merge-conflict magnet. Fixtures do not have to live in conftest files: any importable module can be registered as a plugin, which gives the same visibility with an explicit, per-project dependency.",[19,20,22],"h2",{"id":21},"prerequisites","Prerequisites",[24,25,26,38],"ul",{},[27,28,29,33,34,37],"li",{},[30,31,32],"strong",{},"pytest 7.0+"," — note the rootdir restriction on ",[14,35,36],{},"pytest_plugins"," introduced in 7.0.",[27,39,40,41,46],{},"The resolution order described in ",[42,43,45],"a",{"href":44},"\u002Fadvanced-pytest-architecture-configuration\u002Fmanaging-conftest-hierarchies\u002F","managing conftest hierarchies",".",[19,48,50],{"id":49},"solution","Solution",[10,52,53,54,56],{},"Write the fixtures in an ordinary module. Nothing about it is special — no ",[14,55,16],{}," name, no directory placement rules:",[58,59,64],"pre",{"className":60,"code":61,"language":62,"meta":63,"style":63},"language-python shiki shiki-themes github-light github-dark","# testing_support\u002Fpostgres.py\nimport pytest\n\n@pytest.fixture(scope=\"session\")\ndef pg_dsn(worker_id):\n    # worker_id comes from pytest-xdist; \"master\" when running serially.\n    return f\"postgresql:\u002F\u002F\u002Ftest_{worker_id}\"\n\n@pytest.fixture\ndef pg_session(pg_dsn):\n    conn = connect(pg_dsn)\n    tx = conn.begin()\n    yield Session(bind=conn)\n    tx.rollback()                 # every test starts from the same state\n    conn.close()\n","python","",[14,65,66,74,80,87,93,99,105,111,116,122,128,134,140,146,152],{"__ignoreMap":63},[67,68,71],"span",{"class":69,"line":70},"line",1,[67,72,73],{},"# testing_support\u002Fpostgres.py\n",[67,75,77],{"class":69,"line":76},2,[67,78,79],{},"import pytest\n",[67,81,83],{"class":69,"line":82},3,[67,84,86],{"emptyLinePlaceholder":85},true,"\n",[67,88,90],{"class":69,"line":89},4,[67,91,92],{},"@pytest.fixture(scope=\"session\")\n",[67,94,96],{"class":69,"line":95},5,[67,97,98],{},"def pg_dsn(worker_id):\n",[67,100,102],{"class":69,"line":101},6,[67,103,104],{},"    # worker_id comes from pytest-xdist; \"master\" when running serially.\n",[67,106,108],{"class":69,"line":107},7,[67,109,110],{},"    return f\"postgresql:\u002F\u002F\u002Ftest_{worker_id}\"\n",[67,112,114],{"class":69,"line":113},8,[67,115,86],{"emptyLinePlaceholder":85},[67,117,119],{"class":69,"line":118},9,[67,120,121],{},"@pytest.fixture\n",[67,123,125],{"class":69,"line":124},10,[67,126,127],{},"def pg_session(pg_dsn):\n",[67,129,131],{"class":69,"line":130},11,[67,132,133],{},"    conn = connect(pg_dsn)\n",[67,135,137],{"class":69,"line":136},12,[67,138,139],{},"    tx = conn.begin()\n",[67,141,143],{"class":69,"line":142},13,[67,144,145],{},"    yield Session(bind=conn)\n",[67,147,149],{"class":69,"line":148},14,[67,150,151],{},"    tx.rollback()                 # every test starts from the same state\n",[67,153,155],{"class":69,"line":154},15,[67,156,157],{},"    conn.close()\n",[10,159,160,161,163,164,167],{},"Register it where the fixtures are needed. Inside one repository the simplest route is ",[14,162,36],{}," in the ",[30,165,166],{},"rootdir"," conftest:",[58,169,171],{"className":60,"code":170,"language":62,"meta":63,"style":63},"# conftest.py at the rootdir — the only place pytest_plugins is honoured\npytest_plugins = [\n    \"testing_support.postgres\",\n    \"testing_support.clock\",\n]\n",[14,172,173,178,183,188,193],{"__ignoreMap":63},[67,174,175],{"class":69,"line":70},[67,176,177],{},"# conftest.py at the rootdir — the only place pytest_plugins is honoured\n",[67,179,180],{"class":69,"line":76},[67,181,182],{},"pytest_plugins = [\n",[67,184,185],{"class":69,"line":82},[67,186,187],{},"    \"testing_support.postgres\",\n",[67,189,190],{"class":69,"line":89},[67,191,192],{},"    \"testing_support.clock\",\n",[67,194,195],{"class":69,"line":95},[67,196,197],{},"]\n",[10,199,200,201,204,205,46],{},"For a monorepo where different services need different support modules, an installed plugin is the better fit, because registration then follows the dependency rather than the directory. The same module gains a ",[14,202,203],{},"pytest11"," entry point and each service depends on it explicitly — the mechanics are in ",[42,206,208],{"href":207},"\u002Fadvanced-pytest-architecture-configuration\u002Fbuilding-custom-pytest-plugins\u002Fpackaging-a-pytest-plugin-with-entry-points\u002F","packaging a pytest plugin with entry points",[10,210,211,212,215,216,219],{},"The third route needs no packaging at all: ",[14,213,214],{},"-p"," on the command line, or in ",[14,217,218],{},"addopts"," for one project:",[58,221,225],{"className":222,"code":223,"language":224,"meta":63,"style":63},"language-toml shiki shiki-themes github-light github-dark","# services\u002Fbilling\u002Fpyproject.toml\n[tool.pytest.ini_options]\naddopts = \"-p testing_support.postgres -p testing_support.clock\"\n","toml",[14,226,227,232,237],{"__ignoreMap":63},[67,228,229],{"class":69,"line":70},[67,230,231],{},"# services\u002Fbilling\u002Fpyproject.toml\n",[67,233,234],{"class":69,"line":76},[67,235,236],{},"[tool.pytest.ini_options]\n",[67,238,239],{"class":69,"line":82},[67,240,241],{},"addopts = \"-p testing_support.postgres -p testing_support.clock\"\n",[10,243,244],{},"That form is explicit, local to the project that needs it, and visible in the file a new engineer reads first — which makes it the best default for a monorepo where the support package is not separately versioned.",[246,247,250,351],"figure",{"className":248},[249],"diagram",[251,252,259,260,259,264,259,268,259,276,259,285,259,293,259,297,259,302,259,308,259,312,259,314,259,318,259,321,259,325,259,327,259,331,259,334,259,337,259,339,259,343,259,346],"svg",{"viewBox":253,"role":254,"ariaLabelledBy":255,"xmlns":258},"0 0 760 338","img",[256,257],"fixtureroutes-t","fixtureroutes-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[261,262,263],"title",{"id":256},"Four ways to make a fixture visible",[265,266,267],"desc",{"id":257},"A stack of four registration routes for shared fixtures: a conftest file in the directory tree, pytest_plugins in the rootdir conftest, the -p flag or addopts in a project config, and an installed entry-point plugin, each with its scope and cost.",[269,270],"rect",{"x":271,"y":271,"width":272,"height":273,"rx":274,"fill":275},"0","760","338","14","#fffdf8",[277,278,263],"text",{"x":279,"y":280,"textAnchor":281,"fontSize":282,"fontWeight":283,"fill":284},"380","30","middle","15","700","#3d405b",[269,286],{"x":280,"y":287,"width":283,"height":288,"rx":289,"fill":290,"stroke":291,"strokeWidth":292},"56","52","10","#f4f1de","#e07a5f","1.8",[269,294],{"x":280,"y":287,"width":295,"height":288,"rx":296,"fill":291},"8","4",[277,298,301],{"x":288,"y":299,"fontSize":300,"fontWeight":283,"fill":284},"86","12.5","conftest.py in the tree",[277,303,307],{"x":304,"y":299,"textAnchor":305,"fontSize":306,"fill":284},"714","end","11","automatic for that directory — no opt-in, no opt-out",[269,309],{"x":280,"y":310,"width":283,"height":288,"rx":289,"fill":275,"stroke":311,"strokeWidth":292},"120","#f2cc8f",[269,313],{"x":280,"y":310,"width":295,"height":288,"rx":296,"fill":311},[277,315,317],{"x":288,"y":316,"fontSize":300,"fontWeight":283,"fill":284},"150","pytest_plugins at rootdir",[277,319,320],{"x":304,"y":316,"textAnchor":305,"fontSize":306,"fill":284},"whole repository, one list, rootdir only",[269,322],{"x":280,"y":323,"width":283,"height":288,"rx":289,"fill":290,"stroke":324,"strokeWidth":292},"184","#81b29a",[269,326],{"x":280,"y":323,"width":295,"height":288,"rx":296,"fill":324},[277,328,330],{"x":288,"y":329,"fontSize":300,"fontWeight":283,"fill":284},"214","-p module in addopts",[277,332,333],{"x":304,"y":329,"textAnchor":305,"fontSize":306,"fill":284},"one project, explicit and local",[269,335],{"x":280,"y":336,"width":283,"height":288,"rx":289,"fill":275,"stroke":284,"strokeWidth":292},"248",[269,338],{"x":280,"y":336,"width":295,"height":288,"rx":296,"fill":284},[277,340,342],{"x":288,"y":341,"fontSize":300,"fontWeight":283,"fill":284},"278","installed pytest11 plugin",[277,344,345],{"x":304,"y":341,"textAnchor":305,"fontSize":306,"fill":284},"anything that declares the dependency",[277,347,350],{"x":279,"y":348,"textAnchor":281,"fontSize":306,"fontStyle":349,"fill":284},"330","italic","All four produce fixtures that behave identically once registered.",[352,353,354],"figcaption",{},"The lower three are opt-in, which is the property a growing repository needs; a conftest is the only route with no way to decline it.",[19,356,358],{"id":357},"why-this-works","Why this works",[10,360,361,362,365,366,370,371,374],{},"pytest treats a registered plugin module exactly like a conftest for the purposes of fixture collection: it scans the module for functions decorated with ",[14,363,364],{},"@pytest.fixture"," and adds them to the fixture registry under the names they declare. The difference is ",[367,368,369],"em",{},"when"," and ",[367,372,373],{},"whether",". Conftest files are discovered by directory walk and are mandatory for everything beneath them; plugin modules are registered explicitly and only where declared. Because plugins register before conftest files are loaded, a conftest fixture of the same name shadows the plugin's — the local definition always wins, which is what makes overriding a shared fixture in one directory possible.",[246,376,378,506],{"className":377},[249],[251,379,259,384,259,387,259,390,259,407,259,410,259,412,259,419,259,423,259,427,259,432,259,438,259,443,259,446,259,449,259,453,259,457,259,461,259,465,259,468,259,472,259,476,259,480,259,484,259,488,259,491,259,494,259,498,259,502],{"viewBox":380,"role":254,"ariaLabelledBy":381,"xmlns":258},"0 0 760 434",[382,383],"regorder-t","regorder-d",[261,385,386],{"id":382},"Registration order and what it means for overrides",[265,388,389],{"id":383},"A vertical flow of registration order: installed entry-point plugins register first, then modules named in pytest_plugins at the rootdir, then each conftest from rootdir downwards, and finally the test module itself, with later registrations shadowing earlier ones of the same name.",[391,392,393,394,259],"defs",{},"\n    ",[395,396,403],"marker",{"id":397,"viewBox":398,"refX":399,"refY":400,"markerWidth":401,"markerHeight":401,"orient":402},"regorder-a","0 0 10 10","9","5","7","auto-start-reverse",[404,405],"path",{"d":406,"fill":311},"M0 0 L10 5 L0 10 z",[269,408],{"x":271,"y":271,"width":272,"height":409,"rx":274,"fill":275},"434",[277,411,386],{"x":279,"y":280,"textAnchor":281,"fontSize":282,"fontWeight":283,"fill":284},[269,413],{"x":414,"y":415,"width":416,"height":417,"rx":418,"fill":275,"stroke":311,"strokeWidth":292},"34","58","320","64","12",[277,420,422],{"x":421,"y":299,"textAnchor":281,"fontSize":300,"fontWeight":283,"fill":284},"194","entry-point plugins",[277,424,426],{"x":421,"y":425,"textAnchor":281,"fontSize":306,"fill":284},"103","registered at startup",[69,428],{"x1":421,"y1":429,"x2":421,"y2":430,"stroke":311,"strokeWidth":292,"markerEnd":431},"128","149","url(#regorder-a)",[269,433],{"x":434,"y":417,"width":435,"height":288,"rx":289,"fill":275,"stroke":324,"strokeWidth":436,"strokeDashArray":437},"384","346","1.5",[400,296],[277,439,442],{"x":440,"y":441,"textAnchor":281,"fontSize":306,"fill":284},"557","93","Before any conftest is read",[269,444],{"x":414,"y":445,"width":416,"height":417,"rx":418,"fill":290,"stroke":311,"strokeWidth":292},"156",[277,447,448],{"x":421,"y":323,"textAnchor":281,"fontSize":300,"fontWeight":283,"fill":284},"pytest_plugins modules",[277,450,452],{"x":421,"y":451,"textAnchor":281,"fontSize":306,"fill":284},"201","from the rootdir conftest",[69,454],{"x1":421,"y1":455,"x2":421,"y2":456,"stroke":311,"strokeWidth":292,"markerEnd":431},"226","247",[269,458],{"x":434,"y":459,"width":435,"height":288,"rx":289,"fill":290,"stroke":324,"strokeWidth":436,"strokeDashArray":460},"162",[400,296],[277,462,464],{"x":440,"y":463,"textAnchor":281,"fontSize":306,"fill":284},"191","Rootdir only since pytest 7",[269,466],{"x":414,"y":467,"width":416,"height":417,"rx":418,"fill":275,"stroke":311,"strokeWidth":292},"254",[277,469,471],{"x":421,"y":470,"textAnchor":281,"fontSize":300,"fontWeight":283,"fill":284},"282","conftest files",[277,473,475],{"x":421,"y":474,"textAnchor":281,"fontSize":306,"fill":284},"299","rootdir downwards",[69,477],{"x1":421,"y1":478,"x2":421,"y2":479,"stroke":311,"strokeWidth":292,"markerEnd":431},"324","345",[269,481],{"x":434,"y":482,"width":435,"height":288,"rx":289,"fill":275,"stroke":324,"strokeWidth":436,"strokeDashArray":483},"260",[400,296],[277,485,487],{"x":440,"y":486,"textAnchor":281,"fontSize":306,"fill":284},"289","Deeper shadows shallower",[269,489],{"x":414,"y":490,"width":416,"height":417,"rx":418,"fill":290,"stroke":311,"strokeWidth":292},"352",[277,492,493],{"x":421,"y":279,"textAnchor":281,"fontSize":300,"fontWeight":283,"fill":284},"the test module",[277,495,497],{"x":421,"y":496,"textAnchor":281,"fontSize":306,"fill":284},"397","a local fixture always wins",[269,499],{"x":434,"y":500,"width":435,"height":288,"rx":289,"fill":290,"stroke":324,"strokeWidth":436,"strokeDashArray":501},"358",[400,296],[277,503,505],{"x":440,"y":504,"textAnchor":281,"fontSize":306,"fill":284},"387","The last word on any name",[352,507,508],{},"Later registration shadows earlier: a shared fixture can always be overridden locally, and never the other way round.",[19,510,512],{"id":511},"keeping-the-support-package-cheap","Keeping the support package cheap",[10,514,515],{},"The reason to move fixtures out of a root conftest is cost, so it is worth checking that the move actually removed it.",[10,517,518,519,522],{},"Import time is the measurable part. A root conftest importing SQLAlchemy, boto3 and pandas adds that import cost to every pytest invocation in the repository, including ",[14,520,521],{},"--collect-only"," and including suites that never touch a database. Measure it directly:",[58,524,528],{"className":525,"code":526,"language":527,"meta":63,"style":63},"language-console shiki shiki-themes github-light github-dark","$ python -X importtime -c \"import testing_support.postgres\" 2>&1 | tail -5\nimport time:      1943 |      35128 | sqlalchemy\nimport time:       412 |       2201 | testing_support.postgres\n","console",[14,529,530,535,540],{"__ignoreMap":63},[67,531,532],{"class":69,"line":70},[67,533,534],{},"$ python -X importtime -c \"import testing_support.postgres\" 2>&1 | tail -5\n",[67,536,537],{"class":69,"line":76},[67,538,539],{},"import time:      1943 |      35128 | sqlalchemy\n",[67,541,542],{"class":69,"line":82},[67,543,544],{},"import time:       412 |       2201 | testing_support.postgres\n",[10,546,547,548,551,552,555,556,46],{},"Anything above a few tens of milliseconds is worth deferring. Two techniques do that without changing the fixture API: import the heavy dependency ",[367,549,550],{},"inside"," the fixture body rather than at module level, and split one support module into several so a project registering ",[14,553,554],{},"testing_support.clock"," does not pay for ",[14,557,558],{},"testing_support.postgres",[58,560,562],{"className":60,"code":561,"language":62,"meta":63,"style":63},"# testing_support\u002Fpostgres.py — heavy import deferred to first use\nimport pytest\n\n@pytest.fixture(scope=\"session\")\ndef pg_engine(pg_dsn):\n    from sqlalchemy import create_engine     # paid only when the fixture is used\n    engine = create_engine(pg_dsn)\n    yield engine\n    engine.dispose()\n",[14,563,564,569,573,577,581,586,591,596,601],{"__ignoreMap":63},[67,565,566],{"class":69,"line":70},[67,567,568],{},"# testing_support\u002Fpostgres.py — heavy import deferred to first use\n",[67,570,571],{"class":69,"line":76},[67,572,79],{},[67,574,575],{"class":69,"line":82},[67,576,86],{"emptyLinePlaceholder":85},[67,578,579],{"class":69,"line":89},[67,580,92],{},[67,582,583],{"class":69,"line":95},[67,584,585],{},"def pg_engine(pg_dsn):\n",[67,587,588],{"class":69,"line":101},[67,589,590],{},"    from sqlalchemy import create_engine     # paid only when the fixture is used\n",[67,592,593],{"class":69,"line":107},[67,594,595],{},"    engine = create_engine(pg_dsn)\n",[67,597,598],{"class":69,"line":113},[67,599,600],{},"    yield engine\n",[67,602,603],{"class":69,"line":118},[67,604,605],{},"    engine.dispose()\n",[10,607,608,609,611],{},"The second habit is to keep the modules small and single-purpose. A support package of six focused modules lets each project register exactly what it uses, and it makes the dependency legible: reading ",[14,610,218],{}," tells you what a service's tests actually need, which a four-hundred-line root conftest never did.",[19,613,615],{"id":614},"migrating-an-overgrown-root-conftest","Migrating an overgrown root conftest",[10,617,618],{},"The move from one large conftest to registered modules is mechanical, and doing it in the order below keeps the suite green at every step.",[10,620,621,624,625,628],{},[30,622,623],{},"Step one: split by consumer, not by theme."," Group the fixtures by which directories actually request them, using ",[14,626,627],{},"--fixtures-per-test"," on a sample of tests from each area rather than by reading the file. Fixtures used by one service move to a module named for that service; fixtures used by everything stay where they are for now.",[58,630,632],{"className":525,"code":631,"language":527,"meta":63,"style":63},"$ pytest services\u002Fbilling --fixtures-per-test -q | grep conftest.py | sort -u\npg_session -- conftest.py:44\nfrozen_clock -- conftest.py:71\n",[14,633,634,639,644],{"__ignoreMap":63},[67,635,636],{"class":69,"line":70},[67,637,638],{},"$ pytest services\u002Fbilling --fixtures-per-test -q | grep conftest.py | sort -u\n",[67,640,641],{"class":69,"line":76},[67,642,643],{},"pg_session -- conftest.py:44\n",[67,645,646],{"class":69,"line":82},[67,647,648],{},"frozen_clock -- conftest.py:71\n",[10,650,651,654,655,658,659,662],{},[30,652,653],{},"Step two: move the code, keep the conftest importing it."," Cut the fixtures into ",[14,656,657],{},"testing_support\u002Fpostgres.py"," and add ",[14,660,661],{},"pytest_plugins = [\"testing_support.postgres\"]"," to the same root conftest. Nothing about visibility changes, so the whole suite still passes — this step is a pure refactor and is the one to land on its own.",[10,664,665,668,669,658,671,674,675,677],{},[30,666,667],{},"Step three: narrow the registration."," Remove the module from the root ",[14,670,36],{},[14,672,673],{},"-p testing_support.postgres"," to the ",[14,676,218],{}," of each project that needs it. Now the failure mode is loud: a service that used the fixture without declaring it fails at setup with \"fixture not found\", which is precisely the coupling the migration is meant to surface.",[10,679,680,683],{},[30,681,682],{},"Step four: delete what nothing requests."," Fixtures that no project registered are dead code that was invisible while the root conftest loaded everything. Removing them is the actual payoff, and it is usually a third of the file.",[10,685,686],{},"Do not attempt steps two and three in one commit. The refactor is safe and the narrowing is not, and separating them means a failing service can be fixed — or given back its registration — without reverting the move.",[246,688,690,805],{"className":689},[249],[251,691,259,696,259,699,259,702,259,705,259,707,259,712,259,718,259,723,259,728,259,732,259,737,259,743,259,746,259,749,259,754,259,758,259,762,259,767,259,770,259,773,259,777,259,780,259,783,259,786,259,789,259,792,259,796,259,799,259,802],{"viewBox":692,"role":254,"ariaLabelledBy":693,"xmlns":258},"0 0 760 268",[694,695],"conftestmig-t","conftestmig-d",[261,697,698],{"id":694},"Four steps out of a monolithic conftest",[265,700,701],{"id":695},"A timeline of the migration: group the fixtures by consumer, move them to a module while keeping the rootdir registration, narrow the registration to the projects that declared them, and finally delete the fixtures nothing requested.",[269,703],{"x":271,"y":271,"width":272,"height":704,"rx":274,"fill":275},"268",[277,706,698],{"x":279,"y":280,"textAnchor":281,"fontSize":282,"fontWeight":283,"fill":284},[69,708],{"x1":709,"y1":316,"x2":710,"y2":316,"stroke":284,"strokeWidth":711},"40","720","2",[269,713],{"x":714,"y":715,"width":289,"height":716,"rx":717,"fill":284},"35","136","28","3",[269,719],{"x":418,"y":720,"width":721,"height":722,"rx":289,"fill":275,"stroke":284,"strokeWidth":292},"50","209","72",[277,724,727],{"x":725,"y":726,"textAnchor":281,"fontSize":306,"fontWeight":283,"fill":284},"116","74","step 1",[277,729,731],{"x":725,"y":730,"textAnchor":281,"fontSize":300,"fontWeight":283,"fill":284},"89","group by consumer",[277,733,736],{"x":725,"y":734,"textAnchor":281,"fontSize":735,"fill":284},"106","10.5","use --fixtures-per-test",[69,738],{"x1":709,"y1":739,"x2":709,"y2":740,"stroke":284,"strokeWidth":741,"strokeDashArray":742},"124","134","1.4",[296,717],[269,744],{"x":745,"y":715,"width":289,"height":716,"rx":717,"fill":324},"262",[269,747],{"x":459,"y":748,"width":721,"height":722,"rx":289,"fill":275,"stroke":324,"strokeWidth":292},"176",[277,750,753],{"x":751,"y":752,"textAnchor":281,"fontSize":306,"fontWeight":283,"fill":284},"267","200","step 2",[277,755,757],{"x":751,"y":756,"textAnchor":281,"fontSize":300,"fontWeight":283,"fill":284},"215","move, keep visible",[277,759,761],{"x":751,"y":760,"textAnchor":281,"fontSize":735,"fill":284},"232","a pure refactor",[69,763],{"x1":751,"y1":764,"x2":751,"y2":765,"stroke":324,"strokeWidth":741,"strokeDashArray":766},"166","174",[296,717],[269,768],{"x":769,"y":715,"width":289,"height":716,"rx":717,"fill":311},"488",[269,771],{"x":772,"y":720,"width":721,"height":722,"rx":289,"fill":275,"stroke":311,"strokeWidth":292},"389",[277,774,776],{"x":775,"y":726,"textAnchor":281,"fontSize":306,"fontWeight":283,"fill":284},"493","step 3",[277,778,779],{"x":775,"y":730,"textAnchor":281,"fontSize":300,"fontWeight":283,"fill":284},"narrow registration",[277,781,782],{"x":775,"y":734,"textAnchor":281,"fontSize":735,"fill":284},"coupling becomes loud",[69,784],{"x1":775,"y1":739,"x2":775,"y2":740,"stroke":311,"strokeWidth":741,"strokeDashArray":785},[296,717],[269,787],{"x":788,"y":715,"width":289,"height":716,"rx":717,"fill":291},"715",[269,790],{"x":791,"y":748,"width":721,"height":722,"rx":289,"fill":275,"stroke":291,"strokeWidth":292},"539",[277,793,795],{"x":794,"y":752,"textAnchor":281,"fontSize":306,"fontWeight":283,"fill":284},"644","step 4",[277,797,798],{"x":794,"y":756,"textAnchor":281,"fontSize":300,"fontWeight":283,"fill":284},"delete the unused",[277,800,801],{"x":794,"y":760,"textAnchor":281,"fontSize":735,"fill":284},"usually a third of it",[69,803],{"x1":710,"y1":764,"x2":710,"y2":765,"stroke":291,"strokeWidth":741,"strokeDashArray":804},[296,717],[352,806,807],{},"Steps two and three belong in separate commits: one cannot fail, the other is designed to.",[19,809,811],{"id":810},"edge-cases-and-failure-modes","Edge cases and failure modes",[24,813,814,825,842,852,858],{},[27,815,816,821,822,824],{},[30,817,818,820],{},[14,819,36],{}," in a nested conftest is an error since pytest 7."," The message is explicit, but code moved from an older repository hits it immediately; convert those to ",[14,823,214],{}," in the project config or to an installed plugin.",[27,826,827,830,831,834,835,838,839,841],{},[30,828,829],{},"Plugin modules must be importable from the rootdir."," A ",[14,832,833],{},"src"," layout without an installed package means the module is not on ",[14,836,837],{},"sys.path"," during collection; install the support package (even editable) rather than manipulating ",[14,840,837],{}," in a conftest.",[27,843,844,847,848,851],{},[30,845,846],{},"Fixtures in a plugin cannot see conftest-only fixtures."," Registration order means a plugin fixture requesting a name defined only in a deep conftest resolves at ",[367,849,850],{},"use"," time, so it works in one directory and fails in another. Keep plugin fixtures self-contained.",[27,853,854,857],{},[30,855,856],{},"Autouse fixtures in a shared plugin apply everywhere it is registered."," That is the same repository-wide cost the root conftest had, just relocated — make autouse a deliberate choice per module.",[27,859,860,866],{},[30,861,862,865],{},[14,863,864],{},"--fixtures"," reports the defining file",", which is the fastest way to confirm a fixture is coming from the plugin rather than from a stale conftest copy left behind by the migration.",[10,868,869,870,873],{},"One organisational note for teams doing this at scale: give the support package an owner and a changelog. Shared fixtures are production code for the test suite, and a breaking change to ",[14,871,872],{},"pg_session"," affects every service that registered it — which is exactly the coupling the migration made visible. A one-line changelog entry per behavioural change, plus a deprecation period for renamed fixtures, costs almost nothing and prevents the failure mode where a support-package upgrade breaks four services at once and each team debugs it separately.",[10,875,876,877,880,881,883],{},"There is one visibility difference worth internalising before choosing this route: a fixture in a conftest is discoverable by reading the directory, while a fixture in a registered plugin is discoverable only by reading the registration. That trade is usually worth it — explicit dependencies beat ambient ones — but it puts weight on ",[14,878,879],{},"pytest --fixtures",", which becomes the canonical answer to \"what is available here\". Teach the command alongside the migration, and consider adding a short docstring to every shared fixture, because ",[14,882,864],{}," prints them and a one-line description is the difference between a discoverable API and a list of names.",[19,885,887],{"id":886},"frequently-asked-questions","Frequently Asked Questions",[10,889,890,893],{},[30,891,892],{},"Can I import fixtures directly from another module?","\nImporting the fixture function works but is fragile: the name must be re-exported in the importing module's namespace, and pytest resolves it there rather than at the definition site. Registering the module as a plugin is the supported route.",[10,895,896,899,900,902,903,905,906,908],{},[30,897,898],{},"Where is pytest_plugins allowed?","\nSince pytest 7, ",[14,901,36],{}," is only honoured in the rootdir-level ",[14,904,16],{},". In nested conftest files it raises an error, so use an installed plugin or the ",[14,907,214],{}," flag for directory-specific registration.",[10,910,911,914],{},[30,912,913],{},"Do plugin-provided fixtures override conftest ones?","\nNo, the opposite. Plugins register before conftest files, so a fixture with the same name defined in a conftest shadows the plugin's version for that directory tree.",[19,916,918],{"id":917},"related","Related",[24,920,921,927,934,940],{},[27,922,923,926],{},[42,924,925],{"href":44},"Managing conftest hierarchies"," — the resolution order this builds on.",[27,928,929,933],{},[42,930,932],{"href":931},"\u002Fadvanced-pytest-architecture-configuration\u002Fmanaging-conftest-hierarchies\u002Fcreating-conftestpy-hierarchies-for-monorepos\u002F","Creating conftest.py hierarchies for monorepos"," — service boundaries and the isolation check.",[27,935,936,939],{},[42,937,938],{"href":207},"Packaging a pytest plugin with entry points"," — turning the support module into a real dependency.",[27,941,942,946],{},[42,943,945],{"href":944},"\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002Ffixing-scopemismatch-errors-in-pytest\u002F","Fixing ScopeMismatch errors in pytest"," — the error that appears when shared fixtures are widened during a migration.",[10,948,949,950],{},"← Back to ",[42,951,952],{"href":44},"Managing Conftest Hierarchies",[954,955,956],"style",{},"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);}",{"title":63,"searchDepth":76,"depth":76,"links":958},[959,960,961,962,963,964,965,966],{"id":21,"depth":76,"text":22},{"id":49,"depth":76,"text":50},{"id":357,"depth":76,"text":358},{"id":511,"depth":76,"text":512},{"id":614,"depth":76,"text":615},{"id":810,"depth":76,"text":811},{"id":886,"depth":76,"text":887},{"id":917,"depth":76,"text":918},"Share pytest fixtures across packages with plugin modules and pytest_plugins instead of a growing root conftest: import rules, scope, and the rootdir restriction.","md",{"slug":970,"type":971,"breadcrumb":972,"datePublished":973,"dateModified":973,"faq":974,"howto":981},"sharing-fixtures-without-conftest-py","article","Fixtures Without conftest","2026-08-01",[975,977,979],{"q":892,"a":976},"Importing the fixture function works but is fragile: the name must be re-exported in the importing module's namespace, and pytest resolves it there rather than at the definition site. Registering the module as a plugin is the supported route.",{"q":898,"a":978},"Since pytest 7, pytest_plugins is only honoured in the rootdir-level conftest.py. In nested conftest files it raises an error, so use an installed plugin or the -p flag for directory-specific registration.",{"q":913,"a":980},"No, the opposite. Plugins register before conftest files, so a fixture with the same name defined in a conftest shadows the plugin's version for that directory tree.",{"name":982,"description":983,"steps":984},"How to share pytest fixtures without conftest.py","Move shared fixtures into plugin modules and register them explicitly where they are needed.",[985,988,991,994],{"name":986,"text":987},"Move the fixtures into a normal module","Create an importable module containing the fixtures, with no conftest semantics attached.",{"name":989,"text":990},"Register it as a plugin","List the dotted module path in pytest_plugins in the rootdir conftest, or install it as an entry-point plugin.",{"name":992,"text":993},"Keep registration local to the services that need it","Register per project rather than globally so unrelated suites do not pay the import cost.",{"name":995,"text":996},"Verify with --fixtures","Confirm the fixture is visible and that its reported source file is the plugin module.","\u002Fadvanced-pytest-architecture-configuration\u002Fmanaging-conftest-hierarchies\u002Fsharing-fixtures-without-conftest-py",{"title":5,"description":967},"advanced-pytest-architecture-configuration\u002Fmanaging-conftest-hierarchies\u002Fsharing-fixtures-without-conftest-py\u002Findex","XPUBijg1AEOZY4IL6hTATRhRKAkmY1dVjr6eE8hDy90",1785613404415]