[{"data":1,"prerenderedAt":1141},["ShallowReactive",2],{"page-\u002Fadvanced-pytest-architecture-configuration\u002Fbuilding-custom-pytest-plugins\u002F":3},{"id":4,"title":5,"body":6,"description":1134,"extension":1135,"meta":1136,"navigation":321,"path":1137,"seo":1138,"stem":1139,"__hash__":1140},"content\u002Fadvanced-pytest-architecture-configuration\u002Fbuilding-custom-pytest-plugins\u002Findex.md","Building Custom Pytest Plugins",{"type":7,"value":8,"toc":1123},"minimark",[9,13,27,32,52,55,114,117,127,148,152,159,166,187,204,272,281,285,302,305,357,364,394,411,415,418,495,498,504,539,549,604,620,624,631,637,657,663,688,711,729,733,736,741,886,893,914,918,929,936,996,1001,1050,1072,1077,1089,1100,1109,1119],[10,11,5],"h1",{"id":12},"building-custom-pytest-plugins",[14,15,16,17,21,22,26],"p",{},"Transforming pytest from a generic test runner into a domain-specific testing framework requires a deep understanding of its extension architecture. By leveraging ",[18,19,20],"code",{},"pluggy"," hooks, entry point discovery, and AST-based assertion rewriting, engineering teams can build reusable, production-grade plugins that standardize test behavior across monorepos and open-source ecosystems. This guide details the architectural patterns, lifecycle mechanics, and distribution strategies required for robust ",[23,24,25],"strong",{},"pytest plugin development",".",[28,29,31],"h2",{"id":30},"understanding-pytests-plugin-architecture","Understanding Pytest's Plugin Architecture",[14,33,34,35,37,38,41,42,45,46,51],{},"At its core, pytest relies on ",[18,36,20],{},", a lightweight plugin framework that decouples hook specifications (",[18,39,40],{},"hookspec",") from their implementations (",[18,43,44],{},"hookimpl","). When pytest initializes, it constructs a plugin manager that registers all discovered plugins, resolves hook chains, and executes them in a deterministic order. Understanding the ",[47,48,50],"a",{"href":49},"\u002Fadvanced-pytest-architecture-configuration\u002F","Advanced Pytest Architecture & Configuration"," foundation is critical before extending the runner, as misaligned hook implementations can silently break collection or execution phases.",[14,53,54],{},"The plugin lifecycle follows a strict sequence:",[56,57,58,69,90,102],"ol",{},[59,60,61,68],"li",{},[23,62,63,64,67],{},"Initialization (",[18,65,66],{},"pytest_configure",")",": Plugins parse configuration, register custom markers, and set up global state.",[59,70,71,81,82,85,86,89],{},[23,72,73,74,77,78,67],{},"Collection (",[18,75,76],{},"pytest_collect_file",", ",[18,79,80],{},"pytest_pycollect_makeitem",": Test modules are discovered, parsed, and converted into ",[18,83,84],{},"Item"," and ",[18,87,88],{},"Collector"," objects.",[59,91,92,101],{},[23,93,94,95,77,98,67],{},"Setup & Execution (",[18,96,97],{},"pytest_runtest_setup",[18,99,100],{},"pytest_runtest_call",": Fixtures are resolved, tests execute, and teardown occurs.",[59,103,104,113],{},[23,105,106,107,77,110,67],{},"Reporting (",[18,108,109],{},"pytest_terminal_summary",[18,111,112],{},"pytest_runtest_logreport",": Results are aggregated, formatted, and emitted to stdout\u002Ffiles.",[14,115,116],{},"A minimal production-ready plugin structure separates core logic from packaging metadata:",[118,119,125],"pre",{"className":120,"code":122,"language":123,"meta":124},[121],"language-text","pytest-myplugin\u002F\n├── src\u002F\n│ └── pytest_myplugin\u002F\n│ ├── __init__.py\n│ ├── plugin.py # Hook implementations\n│ └── fixtures.py # Reusable fixture definitions\n├── tests\u002F\n│ └── test_integration.py\n└── pyproject.toml\n","text","",[18,126,122],{"__ignoreMap":124},[14,128,129,130,133,134,136,137,139,140,143,144,147],{},"The ",[18,131,132],{},"plugin.py"," module typically houses ",[18,135,44],{}," decorators. Crucially, ",[18,138,20],{}," resolves hooks by name and executes them in reverse registration order by default, unless ",[18,141,142],{},"tryfirst=True"," or ",[18,145,146],{},"trylast=True"," is specified. This deterministic resolution allows plugins to safely wrap or intercept core pytest behavior without monkeypatching internal modules.",[28,149,151],{"id":150},"registering-and-discovering-custom-plugins","Registering and Discovering Custom Plugins",[14,153,154,155,158],{},"Plugin discovery hinges on Python packaging entry points. When pytest boots, it iterates through installed distributions and scans for the ",[18,156,157],{},"pytest11"," namespace. Any package exposing an entry point under this namespace is automatically imported and registered with the plugin manager.",[14,160,161,162,165],{},"Configure discovery in ",[18,163,164],{},"pyproject.toml"," using PEP 621 standards:",[118,167,171],{"className":168,"code":169,"language":170,"meta":124,"style":124},"language-toml shiki shiki-themes github-light github-dark","[project.entry-points.pytest11]\nmyplugin = \"pytest_myplugin.plugin\"\n","toml",[18,172,173,181],{"__ignoreMap":124},[174,175,178],"span",{"class":176,"line":177},"line",1,[174,179,180],{},"[project.entry-points.pytest11]\n",[174,182,184],{"class":176,"line":183},2,[174,185,186],{},"myplugin = \"pytest_myplugin.plugin\"\n",[14,188,189,190,193,194,196,197,199,200,203],{},"This declarative registration ensures the plugin activates across any project where the package is installed, eliminating the need for manual ",[18,191,192],{},"conftest.py"," imports. However, precedence rules dictate behavior: installed plugins load before local ",[18,195,192],{}," files, but ",[18,198,192],{}," can override plugin fixtures and hooks within its directory tree. Use ",[18,201,202],{},"pytest --trace-config"," to audit the exact load order and verify registration:",[118,205,209],{"className":206,"code":207,"language":208,"meta":124,"style":124},"language-bash shiki shiki-themes github-light github-dark","$ pytest --trace-config -q\nPLUGIN registered: pytest_myplugin.plugin (from: \u002Fpath\u002Fto\u002Fsite-packages)\nPLUGIN registered: _pytest.main (from: \u002Fpath\u002Fto\u002Fsite-packages\u002F_pytest\u002Fmain.py)\n...\n","bash",[18,210,211,228,249,266],{"__ignoreMap":124},[174,212,213,217,221,225],{"class":176,"line":177},[174,214,216],{"class":215},"sScJk","$",[174,218,220],{"class":219},"sZZnC"," pytest",[174,222,224],{"class":223},"sj4cs"," --trace-config",[174,226,227],{"class":223}," -q\n",[174,229,230,233,236,239,243,246],{"class":176,"line":183},[174,231,232],{"class":215},"PLUGIN",[174,234,235],{"class":219}," registered:",[174,237,238],{"class":219}," pytest_myplugin.plugin",[174,240,242],{"class":241},"sVt8B"," (from: ",[174,244,245],{"class":219},"\u002Fpath\u002Fto\u002Fsite-packages",[174,247,248],{"class":241},")\n",[174,250,252,254,256,259,261,264],{"class":176,"line":251},3,[174,253,232],{"class":215},[174,255,235],{"class":219},[174,257,258],{"class":219}," _pytest.main",[174,260,242],{"class":241},[174,262,263],{"class":219},"\u002Fpath\u002Fto\u002Fsite-packages\u002F_pytest\u002Fmain.py",[174,265,248],{"class":241},[174,267,269],{"class":176,"line":268},4,[174,270,271],{"class":223},"...\n",[14,273,274,277,278,280],{},[23,275,276],{},"Engineering Trade-off",": While ",[18,279,192],{}," offers rapid iteration during development, it lacks version control and cross-project portability. Distributed plugins should always be preferred for shared infrastructure, as they enable semantic versioning, dependency pinning, and CI matrix validation.",[28,282,284],{"id":283},"integrating-with-fixtures-and-parametrization","Integrating with Fixtures and Parametrization",[14,286,287,288,291,292,296,297,301],{},"Plugins frequently expose domain-specific fixtures that encapsulate complex setup logic. When defining fixtures within a plugin, scope management and ",[18,289,290],{},"autouse"," behavior require careful consideration to avoid unintended side effects or resource contention. Cross-reference fixture scoping with ",[47,293,295],{"href":294},"\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002F","Mastering Pytest Fixtures"," and dynamic test generation with ",[47,298,300],{"href":299},"\u002Fadvanced-pytest-architecture-configuration\u002Fadvanced-parametrization-techniques\u002F","Advanced Parametrization Techniques"," when discussing plugin-driven test injection.",[14,303,304],{},"A plugin-defined session-scoped fixture for database connection pooling:",[118,306,310],{"className":307,"code":308,"language":309,"meta":124,"style":124},"language-python shiki shiki-themes github-light github-dark","import pytest\n\n@pytest.fixture(scope=\"session\", autouse=True)\ndef db_pool(request):\n config = request.config.getoption(\"--db-url\")\n pool = create_connection_pool(config)\n yield pool\n pool.close_all()\n","python",[18,311,312,317,323,328,333,339,345,351],{"__ignoreMap":124},[174,313,314],{"class":176,"line":177},[174,315,316],{},"import pytest\n",[174,318,319],{"class":176,"line":183},[174,320,322],{"emptyLinePlaceholder":321},true,"\n",[174,324,325],{"class":176,"line":251},[174,326,327],{},"@pytest.fixture(scope=\"session\", autouse=True)\n",[174,329,330],{"class":176,"line":268},[174,331,332],{},"def db_pool(request):\n",[174,334,336],{"class":176,"line":335},5,[174,337,338],{}," config = request.config.getoption(\"--db-url\")\n",[174,340,342],{"class":176,"line":341},6,[174,343,344],{}," pool = create_connection_pool(config)\n",[174,346,348],{"class":176,"line":347},7,[174,349,350],{}," yield pool\n",[174,352,354],{"class":176,"line":353},8,[174,355,356],{}," pool.close_all()\n",[14,358,359,360,363],{},"For dynamic test generation, the ",[18,361,362],{},"pytest_generate_tests"," hook intercepts collection and injects parameter sets before execution. This is particularly valuable for data-driven testing or matrix validation without modifying test signatures:",[118,365,367],{"className":307,"code":366,"language":309,"meta":124,"style":124},"def pytest_generate_tests(metafunc):\n if \"env_config\" in metafunc.fixturenames:\n # Load from plugin config or external matrix\n envs = metafunc.config.getoption(\"--env-matrix\", default=[\"staging\", \"prod\"])\n metafunc.parametrize(\"env_config\", envs, scope=\"function\")\n",[18,368,369,374,379,384,389],{"__ignoreMap":124},[174,370,371],{"class":176,"line":177},[174,372,373],{},"def pytest_generate_tests(metafunc):\n",[174,375,376],{"class":176,"line":183},[174,377,378],{}," if \"env_config\" in metafunc.fixturenames:\n",[174,380,381],{"class":176,"line":251},[174,382,383],{}," # Load from plugin config or external matrix\n",[174,385,386],{"class":176,"line":268},[174,387,388],{}," envs = metafunc.config.getoption(\"--env-matrix\", default=[\"staging\", \"prod\"])\n",[174,390,391],{"class":176,"line":335},[174,392,393],{}," metafunc.parametrize(\"env_config\", envs, scope=\"function\")\n",[14,395,396,399,400,403,404,406,407,410],{},[23,397,398],{},"Parallel Execution Considerations",": When using ",[18,401,402],{},"pytest-xdist",", session-scoped fixtures execute once per worker process. Plugins must implement thread-safe state management or use ",[18,405,402],{},"'s ",[18,408,409],{},"worker_id"," fixture to isolate resources. Global mutable state in plugins will cause race conditions and flaky failures in distributed runs.",[28,412,414],{"id":413},"implementing-custom-hooks-and-reporting","Implementing Custom Hooks and Reporting",[14,416,417],{},"Hook execution order dictates how plugins interact with pytest's internal state. The following table outlines critical reporting and execution hooks:",[419,420,421,440],"table",{},[422,423,424],"thead",{},[425,426,427,431,434,437],"tr",{},[428,429,430],"th",{},"Hook",[428,432,433],{},"Phase",[428,435,436],{},"Purpose",[428,438,439],{},"Execution Guarantee",[441,442,443,460,480],"tbody",{},[425,444,445,451,454,457],{},[446,447,448],"td",{},[18,449,450],{},"pytest_runtest_protocol",[446,452,453],{},"Execution",[446,455,456],{},"Wraps setup\u002Fcall\u002Fteardown",[446,458,459],{},"Called once per test item",[425,461,462,467,470,477],{},[446,463,464],{},[18,465,466],{},"pytest_runtest_makereport",[446,468,469],{},"Reporting",[446,471,472,473,476],{},"Modifies ",[18,474,475],{},"TestReport"," objects",[446,478,479],{},"Invoked after each phase",[425,481,482,486,489,492],{},[446,483,484],{},[18,485,109],{},[446,487,488],{},"Post-run",[446,490,491],{},"Appends to CLI output",[446,493,494],{},"Executed after all tests",[14,496,497],{},"Detail terminal output customization by linking to Implementing custom pytest hooks for reporting when explaining result aggregation.",[14,499,500,501,503],{},"A practical ",[18,502,109],{}," implementation for emitting custom metrics:",[118,505,507],{"className":307,"code":506,"language":309,"meta":124,"style":124},"def pytest_terminal_summary(terminalreporter, exitstatus, config):\n terminalreporter.section(\"Custom Plugin Metrics\", sep=\"=\")\n passed = len(terminalreporter.stats.get(\"passed\", []))\n failed = len(terminalreporter.stats.get(\"failed\", []))\n terminalreporter.write_line(f\"Total Passed: {passed}\")\n terminalreporter.write_line(f\"Total Failed: {failed}\")\n",[18,508,509,514,519,524,529,534],{"__ignoreMap":124},[174,510,511],{"class":176,"line":177},[174,512,513],{},"def pytest_terminal_summary(terminalreporter, exitstatus, config):\n",[174,515,516],{"class":176,"line":183},[174,517,518],{}," terminalreporter.section(\"Custom Plugin Metrics\", sep=\"=\")\n",[174,520,521],{"class":176,"line":251},[174,522,523],{}," passed = len(terminalreporter.stats.get(\"passed\", []))\n",[174,525,526],{"class":176,"line":268},[174,527,528],{}," failed = len(terminalreporter.stats.get(\"failed\", []))\n",[174,530,531],{"class":176,"line":335},[174,532,533],{}," terminalreporter.write_line(f\"Total Passed: {passed}\")\n",[174,535,536],{"class":176,"line":341},[174,537,538],{}," terminalreporter.write_line(f\"Total Failed: {failed}\")\n",[14,540,541,542,544,545,548],{},"For granular execution control, ",[18,543,450],{}," allows plugins to intercept the entire test lifecycle. However, overriding this hook requires explicit delegation to ",[18,546,547],{},"item.runtest()"," and proper exception handling to avoid breaking pytest's internal teardown pipeline:",[118,550,552],{"className":307,"code":551,"language":309,"meta":124,"style":124},"import pytest\n\n@pytest.hookimpl(tryfirst=True)\ndef pytest_runtest_protocol(item, nextitem):\n item.config.pluginmanager.get_plugin(\"capture\").suspendcapture()\n try:\n # Custom pre-execution logic\n yield\n finally:\n item.config.pluginmanager.get_plugin(\"capture\").resumecapture()\n",[18,553,554,558,562,567,572,577,582,587,592,598],{"__ignoreMap":124},[174,555,556],{"class":176,"line":177},[174,557,316],{},[174,559,560],{"class":176,"line":183},[174,561,322],{"emptyLinePlaceholder":321},[174,563,564],{"class":176,"line":251},[174,565,566],{},"@pytest.hookimpl(tryfirst=True)\n",[174,568,569],{"class":176,"line":268},[174,570,571],{},"def pytest_runtest_protocol(item, nextitem):\n",[174,573,574],{"class":176,"line":335},[174,575,576],{}," item.config.pluginmanager.get_plugin(\"capture\").suspendcapture()\n",[174,578,579],{"class":176,"line":341},[174,580,581],{}," try:\n",[174,583,584],{"class":176,"line":347},[174,585,586],{}," # Custom pre-execution logic\n",[174,588,589],{"class":176,"line":353},[174,590,591],{}," yield\n",[174,593,595],{"class":176,"line":594},9,[174,596,597],{}," finally:\n",[174,599,601],{"class":176,"line":600},10,[174,602,603],{}," item.config.pluginmanager.get_plugin(\"capture\").resumecapture()\n",[14,605,606,609,610,612,613,615,616,619],{},[23,607,608],{},"Pitfall",": Failing to call ",[18,611,547],{}," or swallowing exceptions in ",[18,614,450],{}," silently breaks test isolation and corrupts the runner's internal state machine. Always wrap custom logic in ",[18,617,618],{},"try\u002Ffinally"," and delegate execution explicitly.",[28,621,623],{"id":622},"assertion-rewriting-and-custom-validation","Assertion Rewriting and Custom Validation",[14,625,626,627,630],{},"Pytest's assertion rewriting is a compile-time AST transformation that enhances standard ",[18,628,629],{},"assert"," statements with rich introspection. When a plugin requires custom validation logic, it must register its modules for rewriting before they are imported. Explain bytecode interception and introspection by referencing Building custom pytest assertion plugins during the AST transformation walkthrough.",[14,632,633,634,636],{},"Registration occurs during ",[18,635,66],{},":",[118,638,640],{"className":307,"code":639,"language":309,"meta":124,"style":124},"def pytest_configure(config):\n import pytest\n pytest.register_assert_rewrite(\"pytest_myplugin.assertions\")\n",[18,641,642,647,652],{"__ignoreMap":124},[174,643,644],{"class":176,"line":177},[174,645,646],{},"def pytest_configure(config):\n",[174,648,649],{"class":176,"line":183},[174,650,651],{}," import pytest\n",[174,653,654],{"class":176,"line":251},[174,655,656],{}," pytest.register_assert_rewrite(\"pytest_myplugin.assertions\")\n",[14,658,129,659,662],{},[18,660,661],{},"pytest_myplugin\u002Fassertions.py"," module can then define helpers that leverage pytest's internal assertion rewriting:",[118,664,666],{"className":307,"code":665,"language":309,"meta":124,"style":124},"def assert_json_schema_match(response, schema):\n \"\"\"Custom assertion with detailed diff output.\"\"\"\n errors = validate_schema(response, schema)\n assert not errors, f\"Schema validation failed:\\n{format_errors(errors)}\"\n",[18,667,668,673,678,683],{"__ignoreMap":124},[174,669,670],{"class":176,"line":177},[174,671,672],{},"def assert_json_schema_match(response, schema):\n",[174,674,675],{"class":176,"line":183},[174,676,677],{}," \"\"\"Custom assertion with detailed diff output.\"\"\"\n",[174,679,680],{"class":176,"line":251},[174,681,682],{}," errors = validate_schema(response, schema)\n",[174,684,685],{"class":176,"line":268},[174,686,687],{}," assert not errors, f\"Schema validation failed:\\n{format_errors(errors)}\"\n",[14,689,690,691,694,695,698,699,702,703,706,707,710],{},"Under the hood, pytest replaces ",[18,692,693],{},"assert expr"," with ",[18,696,697],{},"assert expr, \"expr\""," and injects ",[18,700,701],{},"pytest_assertion_pass","\u002F",[18,704,705],{},"pytest_assertion_fail"," hooks. When rewriting plugin modules, ensure the import hook is registered before any test imports the module. Otherwise, Python's standard import machinery will cache unrewritten bytecode in ",[18,708,709],{},"__pycache__",", resulting in opaque assertion failures.",[14,712,713,716,717,720,721,724,725,728],{},[23,714,715],{},"Debugging Tip",": Run ",[18,718,719],{},"pytest --assert=plain"," to disable rewriting temporarily and verify baseline behavior. Use ",[18,722,723],{},"PYTHONVERBOSE=1"," to trace import hooks and confirm that ",[18,726,727],{},"pytest._rewrite"," intercepts the target module.",[28,730,732],{"id":731},"real-world-plugin-case-study-network-call-tracing","Real-World Plugin Case Study: Network Call Tracing",[14,734,735],{},"Architecting a VCR-style plugin requires intercepting HTTP clients at the transport layer, caching responses, and replaying them deterministically. Demonstrate practical HTTP interception by linking to Tracing network calls with pytest-recording when covering session fixtures and external dependency mocking.",[14,737,738,739,636],{},"The plugin initializes a session-scoped recorder in ",[18,740,66],{},[118,742,744],{"className":307,"code":743,"language":309,"meta":124,"style":124},"import pytest\nimport requests\nfrom unittest.mock import patch\n\ndef pytest_configure(config):\n config.pluginmanager.register(NetworkTracerPlugin(config))\n\nclass NetworkTracerPlugin:\n def __init__(self, config):\n self.config = config\n self.cassette_path = config.getoption(\"--record-mode\", default=\"replay\")\n\n @pytest.hookimpl(tryfirst=True)\n def pytest_sessionstart(self, session):\n self._monkeypatch_requests()\n\n def _monkeypatch_requests(self):\n original_send = requests.Session.send\n def patched_send(self, request, *args, **kwargs):\n key = f\"{request.method}:{request.url}\"\n if self.cassette_path == \"record\":\n resp = original_send(self, request, *args, **kwargs)\n save_cassette(key, resp)\n return resp\n return load_cassette(key)\n requests.Session.send = patched_send\n",[18,745,746,750,755,760,764,768,773,777,782,787,792,798,803,809,815,821,826,832,838,844,850,856,862,868,874,880],{"__ignoreMap":124},[174,747,748],{"class":176,"line":177},[174,749,316],{},[174,751,752],{"class":176,"line":183},[174,753,754],{},"import requests\n",[174,756,757],{"class":176,"line":251},[174,758,759],{},"from unittest.mock import patch\n",[174,761,762],{"class":176,"line":268},[174,763,322],{"emptyLinePlaceholder":321},[174,765,766],{"class":176,"line":335},[174,767,646],{},[174,769,770],{"class":176,"line":341},[174,771,772],{}," config.pluginmanager.register(NetworkTracerPlugin(config))\n",[174,774,775],{"class":176,"line":347},[174,776,322],{"emptyLinePlaceholder":321},[174,778,779],{"class":176,"line":353},[174,780,781],{},"class NetworkTracerPlugin:\n",[174,783,784],{"class":176,"line":594},[174,785,786],{}," def __init__(self, config):\n",[174,788,789],{"class":176,"line":600},[174,790,791],{}," self.config = config\n",[174,793,795],{"class":176,"line":794},11,[174,796,797],{}," self.cassette_path = config.getoption(\"--record-mode\", default=\"replay\")\n",[174,799,801],{"class":176,"line":800},12,[174,802,322],{"emptyLinePlaceholder":321},[174,804,806],{"class":176,"line":805},13,[174,807,808],{}," @pytest.hookimpl(tryfirst=True)\n",[174,810,812],{"class":176,"line":811},14,[174,813,814],{}," def pytest_sessionstart(self, session):\n",[174,816,818],{"class":176,"line":817},15,[174,819,820],{}," self._monkeypatch_requests()\n",[174,822,824],{"class":176,"line":823},16,[174,825,322],{"emptyLinePlaceholder":321},[174,827,829],{"class":176,"line":828},17,[174,830,831],{}," def _monkeypatch_requests(self):\n",[174,833,835],{"class":176,"line":834},18,[174,836,837],{}," original_send = requests.Session.send\n",[174,839,841],{"class":176,"line":840},19,[174,842,843],{}," def patched_send(self, request, *args, **kwargs):\n",[174,845,847],{"class":176,"line":846},20,[174,848,849],{}," key = f\"{request.method}:{request.url}\"\n",[174,851,853],{"class":176,"line":852},21,[174,854,855],{}," if self.cassette_path == \"record\":\n",[174,857,859],{"class":176,"line":858},22,[174,860,861],{}," resp = original_send(self, request, *args, **kwargs)\n",[174,863,865],{"class":176,"line":864},23,[174,866,867],{}," save_cassette(key, resp)\n",[174,869,871],{"class":176,"line":870},24,[174,872,873],{}," return resp\n",[174,875,877],{"class":176,"line":876},25,[174,878,879],{}," return load_cassette(key)\n",[174,881,883],{"class":176,"line":882},26,[174,884,885],{}," requests.Session.send = patched_send\n",[14,887,888,889,892],{},"This approach avoids modifying test code while providing deterministic network behavior. For CI environments, the plugin should default to ",[18,890,891],{},"replay"," mode and fail fast if a cassette is missing. Thread safety is maintained by isolating cassette I\u002FO per worker process and using file locking for concurrent writes.",[14,894,895,898,899,902,903,77,906,909,910,913],{},[23,896,897],{},"Trade-off Analysis",": Monkeypatching at the ",[18,900,901],{},"requests.Session"," level intercepts all downstream libraries (e.g., ",[18,904,905],{},"httpx",[18,907,908],{},"aiohttp"," if wrapped). However, it bypasses lower-level socket mocking. For strict isolation, consider intercepting at the ",[18,911,912],{},"urllib3"," connection pool level instead.",[28,915,917],{"id":916},"packaging-testing-and-distribution","Packaging, Testing, and Distribution",[14,919,920,921,924,925,928],{},"Production plugins require rigorous integration testing before publication. The ",[18,922,923],{},"pytester"," fixture, provided by ",[18,926,927],{},"pytest-dev",", creates isolated temporary environments, writes test files programmatically, and executes pytest subprocesses to validate output and exit codes.",[14,930,931,932,935],{},"A ",[18,933,934],{},"tox.ini"," configuration for multi-environment validation:",[118,937,941],{"className":938,"code":939,"language":940,"meta":124,"style":124},"language-ini shiki shiki-themes github-light github-dark","[tox]\nenvlist = py39, py310, py311, py312, lint\n\n[testenv]\ndeps = pytest>=7.0\ncommands = pytest tests\u002F -v\n\n[testenv:lint]\ndeps = ruff, mypy\ncommands = ruff check src\u002F tests\u002F\n mypy src\u002F\n","ini",[18,942,943,948,953,957,962,967,972,976,981,986,991],{"__ignoreMap":124},[174,944,945],{"class":176,"line":177},[174,946,947],{},"[tox]\n",[174,949,950],{"class":176,"line":183},[174,951,952],{},"envlist = py39, py310, py311, py312, lint\n",[174,954,955],{"class":176,"line":251},[174,956,322],{"emptyLinePlaceholder":321},[174,958,959],{"class":176,"line":268},[174,960,961],{},"[testenv]\n",[174,963,964],{"class":176,"line":335},[174,965,966],{},"deps = pytest>=7.0\n",[174,968,969],{"class":176,"line":341},[174,970,971],{},"commands = pytest tests\u002F -v\n",[174,973,974],{"class":176,"line":347},[174,975,322],{"emptyLinePlaceholder":321},[174,977,978],{"class":176,"line":353},[174,979,980],{},"[testenv:lint]\n",[174,982,983],{"class":176,"line":594},[174,984,985],{},"deps = ruff, mypy\n",[174,987,988],{"class":176,"line":600},[174,989,990],{},"commands = ruff check src\u002F tests\u002F\n",[174,992,993],{"class":176,"line":794},[174,994,995],{}," mypy src\u002F\n",[14,997,998,999,636],{},"Integration test using ",[18,1000,923],{},[118,1002,1004],{"className":307,"code":1003,"language":309,"meta":124,"style":124},"def test_plugin_registers_hook(pytester):\n pytester.makeconftest(\"\"\"\n import pytest\n def pytest_configure(config):\n config.pluginmanager.register(MyPlugin())\n \"\"\")\n result = pytester.runpytest(\"--help\")\n result.stdout.fnmatch_lines([\"*--myplugin-option*\"])\n assert result.ret == 0\n",[18,1005,1006,1011,1016,1020,1025,1030,1035,1040,1045],{"__ignoreMap":124},[174,1007,1008],{"class":176,"line":177},[174,1009,1010],{},"def test_plugin_registers_hook(pytester):\n",[174,1012,1013],{"class":176,"line":183},[174,1014,1015],{}," pytester.makeconftest(\"\"\"\n",[174,1017,1018],{"class":176,"line":251},[174,1019,651],{},[174,1021,1022],{"class":176,"line":268},[174,1023,1024],{}," def pytest_configure(config):\n",[174,1026,1027],{"class":176,"line":335},[174,1028,1029],{}," config.pluginmanager.register(MyPlugin())\n",[174,1031,1032],{"class":176,"line":341},[174,1033,1034],{}," \"\"\")\n",[174,1036,1037],{"class":176,"line":347},[174,1038,1039],{}," result = pytester.runpytest(\"--help\")\n",[174,1041,1042],{"class":176,"line":353},[174,1043,1044],{}," result.stdout.fnmatch_lines([\"*--myplugin-option*\"])\n",[174,1046,1047],{"class":176,"line":594},[174,1048,1049],{}," assert result.ret == 0\n",[14,1051,1052,1053,1056,1057,1060,1061,1064,1065,1068,1069,1071],{},"Publishing follows standard PyPI workflows: ",[18,1054,1055],{},"python -m build"," generates source and wheel distributions, while ",[18,1058,1059],{},"twine upload dist\u002F*"," publishes them. Enforce semantic versioning and pin ",[18,1062,1063],{},"pytest"," compatibility in ",[18,1066,1067],{},"project.dependencies",". Always test against the latest pytest minor release in CI to catch breaking changes in ",[18,1070,20],{}," or internal APIs before users encounter them.",[1073,1074,1076],"h3",{"id":1075},"frequently-asked-questions","Frequently Asked Questions",[14,1078,1079,1082,1083,1085,1086,1088],{},[23,1080,1081],{},"How do I test a custom pytest plugin without installing it globally?","\nUse the ",[18,1084,923],{}," fixture provided by ",[18,1087,927],{},". It creates an isolated temporary environment, writes test files, and runs pytest programmatically to assert expected output and exit codes.",[14,1090,1091,1097,1099],{},[23,1092,1093,1094,1096],{},"What is the difference between ",[18,1095,192],{}," and a distributed plugin?",[18,1098,192],{}," is local to a directory tree and auto-discovered, while distributed plugins are registered via entry points and activated across projects. Plugins are preferred for reusable, versioned extensions.",[14,1101,1102,1105,1106,1108],{},[23,1103,1104],{},"Can a custom plugin modify test parametrization dynamically?","\nYes, via the ",[18,1107,362],{}," hook. It intercepts collection and injects parameter sets before execution, enabling data-driven testing without modifying test functions directly.",[14,1110,1111,1114,1115,1118],{},[23,1112,1113],{},"How does pytest handle assertion rewriting in plugins?","\nPlugins register modules for rewriting using ",[18,1116,1117],{},"pytest.register_assert_rewrite()",". pytest intercepts import hooks, compiles AST with enhanced failure introspection, and caches bytecode for subsequent runs.",[1120,1121,1122],"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);}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 .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}",{"title":124,"searchDepth":183,"depth":183,"links":1124},[1125,1126,1127,1128,1129,1130,1131],{"id":30,"depth":183,"text":31},{"id":150,"depth":183,"text":151},{"id":283,"depth":183,"text":284},{"id":413,"depth":183,"text":414},{"id":622,"depth":183,"text":623},{"id":731,"depth":183,"text":732},{"id":916,"depth":183,"text":917,"children":1132},[1133],{"id":1075,"depth":251,"text":1076},"Transforming pytest from a generic test runner into a domain-specific testing framework requires a deep understanding of its extension architecture. By leveraging pluggy hooks, entry point discovery, and AST-based assertion rewriting, engineering teams can build reusable, production-grade plugins that standardize test behavior across monorepos and open-source ecosystems. This guide details the architectural patterns, lifecycle mechanics, and distribution strategies required for robust pytest plugin development.","md",{},"\u002Fadvanced-pytest-architecture-configuration\u002Fbuilding-custom-pytest-plugins",{"title":5,"description":1134},"advanced-pytest-architecture-configuration\u002Fbuilding-custom-pytest-plugins\u002Findex","0eycO_2QpCvgSb5t76mdPuRQ3qK37EgpgS877DBm-hE",1778004578486]