[{"data":1,"prerenderedAt":1245},["ShallowReactive",2],{"page-\u002Fadvanced-mocking-test-doubles-in-python\u002Ffaking-the-filesystem-and-environment\u002Fusing-tmp-path-instead-of-tempfile-in-tests\u002F":3},{"id":4,"title":5,"body":6,"description":1210,"extension":1211,"meta":1212,"navigation":97,"path":1241,"seo":1242,"stem":1243,"__hash__":1244},"content\u002Fadvanced-mocking-test-doubles-in-python\u002Ffaking-the-filesystem-and-environment\u002Fusing-tmp-path-instead-of-tempfile-in-tests\u002Findex.md","Using tmp_path Instead of tempfile in Tests",{"type":7,"value":8,"toc":1199},"minimark",[9,29,34,65,69,75,163,170,180,224,230,355,359,380,486,490,493,530,536,545,552,563,585,589,592,628,696,728,732,737,866,873,884,960,964,1023,1093,1097,1103,1112,1125,1154,1158,1189,1195],[10,11,12,16,17,20,21,24,25,28],"p",{},[13,14,15],"code",{},"tempfile.mkdtemp()"," in a test works until something goes wrong. Then the cleanup line never runs, the directory accumulates in ",[13,18,19],{},"\u002Ftmp",", and — because the path was reused or guessable — two parallel workers write to it at once and produce a failure that only appears with ",[13,22,23],{},"-n",". ",[13,26,27],{},"tmp_path"," solves all three problems and costs one argument.",[30,31,33],"h2",{"id":32},"prerequisites","Prerequisites",[35,36,37,56],"ul",{},[38,39,40,44,45,47,48,51,52,55],"li",{},[41,42,43],"strong",{},"pytest 7.0+"," (",[13,46,27],{}," and ",[13,49,50],{},"tmp_path_factory"," return ",[13,53,54],{},"pathlib.Path",").",[38,57,58,59,64],{},"The wider isolation picture in ",[60,61,63],"a",{"href":62},"\u002Fadvanced-mocking-test-doubles-in-python\u002Ffaking-the-filesystem-and-environment\u002F","faking the filesystem and environment",".",[30,66,68],{"id":67},"solution","Solution",[10,70,71,72,74],{},"Take ",[13,73,27],{}," as an argument and build whatever the code needs inside it:",[76,77,82],"pre",{"className":78,"code":79,"language":80,"meta":81,"style":81},"language-python shiki shiki-themes github-light github-dark","import json\n\ndef test_writes_report_to_the_target_directory(tmp_path):\n    source = tmp_path \u002F \"input.json\"\n    source.write_text(json.dumps({\"rows\": [1, 2, 3]}), encoding=\"utf-8\")\n    out_dir = tmp_path \u002F \"out\"\n    out_dir.mkdir()\n\n    write_report(source, out_dir)\n\n    report = out_dir \u002F \"report.csv\"\n    assert report.exists()\n    assert report.read_text(encoding=\"utf-8\").startswith(\"row,value\")\n","python","",[13,83,84,92,99,105,111,117,123,129,134,140,145,151,157],{"__ignoreMap":81},[85,86,89],"span",{"class":87,"line":88},"line",1,[85,90,91],{},"import json\n",[85,93,95],{"class":87,"line":94},2,[85,96,98],{"emptyLinePlaceholder":97},true,"\n",[85,100,102],{"class":87,"line":101},3,[85,103,104],{},"def test_writes_report_to_the_target_directory(tmp_path):\n",[85,106,108],{"class":87,"line":107},4,[85,109,110],{},"    source = tmp_path \u002F \"input.json\"\n",[85,112,114],{"class":87,"line":113},5,[85,115,116],{},"    source.write_text(json.dumps({\"rows\": [1, 2, 3]}), encoding=\"utf-8\")\n",[85,118,120],{"class":87,"line":119},6,[85,121,122],{},"    out_dir = tmp_path \u002F \"out\"\n",[85,124,126],{"class":87,"line":125},7,[85,127,128],{},"    out_dir.mkdir()\n",[85,130,132],{"class":87,"line":131},8,[85,133,98],{"emptyLinePlaceholder":97},[85,135,137],{"class":87,"line":136},9,[85,138,139],{},"    write_report(source, out_dir)\n",[85,141,143],{"class":87,"line":142},10,[85,144,98],{"emptyLinePlaceholder":97},[85,146,148],{"class":87,"line":147},11,[85,149,150],{},"    report = out_dir \u002F \"report.csv\"\n",[85,152,154],{"class":87,"line":153},12,[85,155,156],{},"    assert report.exists()\n",[85,158,160],{"class":87,"line":159},13,[85,161,162],{},"    assert report.read_text(encoding=\"utf-8\").startswith(\"row,value\")\n",[10,164,165,166,169],{},"There is no cleanup code, no ",[13,167,168],{},"try\u002Ffinally",", and no shared path. Each test gets a directory named after itself under a per-run base directory, so two tests cannot collide and neither can two xdist workers.",[10,171,172,173,175,176,179],{},"For a fixture wider than function scope — a database seeded from a file, a checkout of a sample repository — ",[13,174,27],{}," cannot be used, because it is function-scoped. Its factory sibling is the correct answer, and the reason ",[13,177,178],{},"ScopeMismatch"," appears when you reach for the wrong one:",[76,181,183],{"className":78,"code":182,"language":80,"meta":81,"style":81},"import pytest\n\n@pytest.fixture(scope=\"session\")\ndef sample_repo(tmp_path_factory):\n    root = tmp_path_factory.mktemp(\"repo\")        # session-scoped directory\n    (root \u002F \"README.md\").write_text(\"# sample\\n\")\n    clone_fixtures_into(root)                     # expensive, done once\n    return root\n",[13,184,185,190,194,199,204,209,214,219],{"__ignoreMap":81},[85,186,187],{"class":87,"line":88},[85,188,189],{},"import pytest\n",[85,191,192],{"class":87,"line":94},[85,193,98],{"emptyLinePlaceholder":97},[85,195,196],{"class":87,"line":101},[85,197,198],{},"@pytest.fixture(scope=\"session\")\n",[85,200,201],{"class":87,"line":107},[85,202,203],{},"def sample_repo(tmp_path_factory):\n",[85,205,206],{"class":87,"line":113},[85,207,208],{},"    root = tmp_path_factory.mktemp(\"repo\")        # session-scoped directory\n",[85,210,211],{"class":87,"line":119},[85,212,213],{},"    (root \u002F \"README.md\").write_text(\"# sample\\n\")\n",[85,215,216],{"class":87,"line":125},[85,217,218],{},"    clone_fixtures_into(root)                     # expensive, done once\n",[85,220,221],{"class":87,"line":131},[85,222,223],{},"    return root\n",[10,225,226,229],{},[13,227,228],{},"mktemp"," returns a fresh directory each call, with a numeric suffix, so a session fixture can hand out several without collision.",[231,232,235,351],"figure",{"className":233},[234],"diagram",[236,237,244,245,244,249,244,253,244,261,244,270,244,279,244,285,244,289,244,293,244,298,244,302,244,305,244,309,244,312,244,315,244,317,244,319,244,322,244,325,244,328,244,330,244,333,244,337,244,340,244,344,244,348],"svg",{"viewBox":238,"role":239,"ariaLabelledBy":240,"xmlns":243},"0 0 760 270","img",[241,242],"tmpcompare-t","tmpcompare-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[246,247,248],"title",{"id":241},"tmp_path against the alternatives",[250,251,252],"desc",{"id":242},"A table comparing tmp_path, tmp_path_factory, tempfile.mkdtemp and a hardcoded path under slash tmp, across automatic cleanup, parallel safety, and whether the files survive a failure for inspection.",[254,255],"rect",{"x":256,"y":256,"width":257,"height":258,"rx":259,"fill":260},"0","760","270","14","#fffdf8",[262,263,248],"text",{"x":264,"y":265,"textAnchor":266,"fontSize":267,"fontWeight":268,"fill":269},"380","30","middle","15","700","#3d405b",[254,271],{"x":272,"y":273,"width":274,"height":275,"rx":276,"fill":277,"stroke":269,"strokeWidth":278},"24","52","712","40","10","#f2cc8f","1.5",[262,280,284],{"x":281,"y":282,"fontSize":283,"fontWeight":268,"fill":269},"38","76","12","Criterion",[262,286,288],{"x":287,"y":282,"textAnchor":266,"fontSize":283,"fontWeight":268,"fill":269},"390","Cleanup",[262,290,292],{"x":291,"y":282,"textAnchor":266,"fontSize":283,"fontWeight":268,"fill":269},"620","Parallel safe",[254,294],{"x":272,"y":295,"width":274,"height":275,"fill":296,"stroke":269,"strokeWidth":297},"92","#f4f1de","1",[262,299,27],{"x":281,"y":300,"fontSize":301,"fill":269},"116","11.5",[262,303,304],{"x":287,"y":300,"textAnchor":266,"fontSize":301,"fill":269},"automatic, delayed",[262,306,308],{"x":291,"y":300,"textAnchor":266,"fontSize":301,"fill":307},"#2a5f49","yes",[254,310],{"x":272,"y":311,"width":274,"height":275,"fill":260,"stroke":269,"strokeWidth":297},"132",[262,313,50],{"x":281,"y":314,"fontSize":301,"fill":269},"156",[262,316,304],{"x":287,"y":314,"textAnchor":266,"fontSize":301,"fill":269},[262,318,308],{"x":291,"y":314,"textAnchor":266,"fontSize":301,"fill":307},[254,320],{"x":272,"y":321,"width":274,"height":275,"fill":296,"stroke":269,"strokeWidth":297},"172",[262,323,15],{"x":281,"y":324,"fontSize":301,"fill":269},"196",[262,326,327],{"x":287,"y":324,"textAnchor":266,"fontSize":301,"fill":269},"manual, skipped on error",[262,329,308],{"x":291,"y":324,"textAnchor":266,"fontSize":301,"fill":307},[254,331],{"x":272,"y":332,"width":274,"height":275,"fill":260,"stroke":269,"strokeWidth":297},"212",[262,334,336],{"x":281,"y":335,"fontSize":301,"fill":269},"236","hardcoded \u002Ftmp\u002Ftest",[262,338,339],{"x":287,"y":335,"textAnchor":266,"fontSize":301,"fill":269},"manual",[262,341,343],{"x":291,"y":335,"textAnchor":266,"fontSize":301,"fill":342},"#8f3d22","collides",[87,345],{"x1":346,"y1":273,"x2":346,"y2":347,"stroke":269,"strokeWidth":297},"274","252",[87,349],{"x1":350,"y1":273,"x2":350,"y2":347,"stroke":269,"strokeWidth":297},"505",[352,353,354],"figcaption",{},"The delayed cleanup is a feature: the files from the last few runs are still there when you need to see what the test actually wrote.",[30,356,358],{"id":357},"why-this-works","Why this works",[10,360,361,362,365,366,368,369,372,373,375,376,379],{},"pytest creates one base temporary directory per run — ",[13,363,364],{},"\u002Ftmp\u002Fpytest-of-\u003Cuser>\u002Fpytest-\u003Cn>\u002F"," — and derives every ",[13,367,27],{}," from it using the test's name, truncated and de-duplicated. Under ",[13,370,371],{},"pytest-xdist"," the worker id is part of that base, so paths are unique across processes by construction. At the end of a run pytest removes base directories older than the last three, which is what makes the files inspectable after a failure without letting them accumulate forever. Because the fixture returns a ",[13,374,54],{},", tests compose paths with ",[13,377,378],{},"\u002F"," and never build platform-specific separators by hand.",[231,381,383,483],{"className":382},[234],[236,384,244,389,244,392,244,395,244,412,244,414,244,416,244,422,244,427,244,432,244,438,244,441,244,446,244,450,244,454,244,457,244,461,244,464,244,468,244,471,244,475,244,478],{"viewBox":385,"role":239,"ariaLabelledBy":386,"xmlns":243},"0 0 760 172",[387,388],"tmplayout-t","tmplayout-d",[246,390,391],{"id":387},"How a per-test directory is derived",[250,393,394],{"id":388},"A left-to-right derivation: a base directory per user, a numbered directory per run, a worker suffix under xdist, and finally a directory named after the test itself.",[396,397,398,399,244],"defs",{},"\n    ",[400,401,408],"marker",{"id":402,"viewBox":403,"refX":404,"refY":405,"markerWidth":406,"markerHeight":406,"orient":407},"tmplayout-a","0 0 10 10","9","5","7","auto-start-reverse",[409,410],"path",{"d":411,"fill":277},"M0 0 L10 5 L0 10 z",[254,413],{"x":256,"y":256,"width":257,"height":321,"rx":259,"fill":260},[262,415,391],{"x":264,"y":265,"textAnchor":266,"fontSize":267,"fontWeight":268,"fill":269},[254,417],{"x":418,"y":419,"width":420,"height":282,"rx":283,"fill":296,"stroke":277,"strokeWidth":421},"26","62","142","1.8",[262,423,426],{"x":424,"y":425,"textAnchor":266,"fontSize":301,"fontWeight":268,"fill":269},"97","96","\u002Ftmp\u002Fpytest-of-user",[262,428,431],{"x":424,"y":429,"textAnchor":266,"fontSize":430,"fill":269},"112","11","per user",[87,433],{"x1":434,"y1":435,"x2":436,"y2":435,"stroke":277,"strokeWidth":421,"markerEnd":437},"176","100","208","url(#tmplayout-a)",[254,439],{"x":440,"y":419,"width":420,"height":282,"rx":283,"fill":260,"stroke":277,"strokeWidth":421},"214",[262,442,445],{"x":443,"y":425,"textAnchor":266,"fontSize":444,"fontWeight":268,"fill":269},"286","12.5","pytest-\u003Cn>",[262,447,449],{"x":443,"y":448,"textAnchor":266,"fontSize":430,"fill":269},"113","per run",[87,451],{"x1":452,"y1":435,"x2":453,"y2":435,"stroke":277,"strokeWidth":421,"markerEnd":437},"364","396",[254,455],{"x":456,"y":419,"width":420,"height":282,"rx":283,"fill":296,"stroke":277,"strokeWidth":421},"403",[262,458,460],{"x":459,"y":425,"textAnchor":266,"fontSize":444,"fontWeight":268,"fill":269},"474","popen-gw0",[262,462,463],{"x":459,"y":448,"textAnchor":266,"fontSize":430,"fill":269},"per worker",[87,465],{"x1":466,"y1":435,"x2":467,"y2":435,"stroke":277,"strokeWidth":421,"markerEnd":437},"552","584",[254,469],{"x":470,"y":419,"width":420,"height":282,"rx":283,"fill":260,"stroke":277,"strokeWidth":421},"592",[262,472,474],{"x":473,"y":425,"textAnchor":266,"fontSize":444,"fontWeight":268,"fill":269},"663","test_name0",[262,476,477],{"x":473,"y":448,"textAnchor":266,"fontSize":430,"fill":269},"per test",[262,479,482],{"x":264,"y":480,"textAnchor":266,"fontSize":301,"fontStyle":481,"fill":269},"164","italic","pytest keeps the last three run directories, then removes older ones.",[352,484,485],{},"Uniqueness at every level is what makes the fixture safe under parallel execution without any configuration.",[30,487,489],{"id":488},"inspecting-what-a-failing-test-wrote","Inspecting what a failing test wrote",[10,491,492],{},"The retained directories turn a class of opaque failure into a two-command investigation. When an assertion about file contents fails, the files are still there:",[76,494,498],{"className":495,"code":496,"language":497,"meta":81,"style":81},"language-console shiki shiki-themes github-light github-dark","$ pytest tests\u002Ftest_reports.py -q\nFAILED tests\u002Ftest_reports.py::test_writes_report - assert 'row,value' in ''\n...\n$ ls \u002Ftmp\u002Fpytest-of-$USER\u002Fpytest-current\u002Ftest_writes_report0\u002F\ninput.json  out\u002F\n$ cat \u002Ftmp\u002Fpytest-of-$USER\u002Fpytest-current\u002Ftest_writes_report0\u002Fout\u002Freport.csv\n","console",[13,499,500,505,510,515,520,525],{"__ignoreMap":81},[85,501,502],{"class":87,"line":88},[85,503,504],{},"$ pytest tests\u002Ftest_reports.py -q\n",[85,506,507],{"class":87,"line":94},[85,508,509],{},"FAILED tests\u002Ftest_reports.py::test_writes_report - assert 'row,value' in ''\n",[85,511,512],{"class":87,"line":101},[85,513,514],{},"...\n",[85,516,517],{"class":87,"line":107},[85,518,519],{},"$ ls \u002Ftmp\u002Fpytest-of-$USER\u002Fpytest-current\u002Ftest_writes_report0\u002F\n",[85,521,522],{"class":87,"line":113},[85,523,524],{},"input.json  out\u002F\n",[85,526,527],{"class":87,"line":119},[85,528,529],{},"$ cat \u002Ftmp\u002Fpytest-of-$USER\u002Fpytest-current\u002Ftest_writes_report0\u002Fout\u002Freport.csv\n",[10,531,532,535],{},[13,533,534],{},"pytest-current"," is a symlink to the most recent run, which makes the path stable enough to use in a shell alias. In CI, point the base elsewhere and collect it as an artefact so the same inspection is possible after the fact:",[76,537,539],{"className":495,"code":538,"language":497,"meta":81,"style":81},"$ pytest --basetemp=build\u002Fpytest-tmp -q\n",[13,540,541],{"__ignoreMap":81},[85,542,543],{"class":87,"line":88},[85,544,538],{},[10,546,547,548,551],{},"Note that ",[13,549,550],{},"--basetemp"," is wiped at the start of each run, so it must be a directory you do not otherwise use — passing a project directory there deletes its contents.",[10,553,554,555,558,559,562],{},"Retention is configurable when the defaults do not suit. ",[13,556,557],{},"tmp_path_retention_count"," sets how many run directories are kept, and ",[13,560,561],{},"tmp_path_retention_policy"," chooses between keeping all of them, only failed ones, or none:",[76,564,568],{"className":565,"code":566,"language":567,"meta":81,"style":81},"language-toml shiki shiki-themes github-light github-dark","[tool.pytest.ini_options]\ntmp_path_retention_count = 3\ntmp_path_retention_policy = \"failed\"     # keep only what you might want to read\n","toml",[13,569,570,575,580],{"__ignoreMap":81},[85,571,572],{"class":87,"line":88},[85,573,574],{},"[tool.pytest.ini_options]\n",[85,576,577],{"class":87,"line":94},[85,578,579],{},"tmp_path_retention_count = 3\n",[85,581,582],{"class":87,"line":101},[85,583,584],{},"tmp_path_retention_policy = \"failed\"     # keep only what you might want to read\n",[30,586,588],{"id":587},"migrating-an-existing-suite","Migrating an existing suite",[10,590,591],{},"The conversion is mechanical and worth doing in one pass, because a suite with both patterns keeps the failure modes of the worse one.",[10,593,594,595,598,599,602,603,606,607,610,611,613,614,617,618,620,621,623,624,627],{},"Search for the three shapes: ",[13,596,597],{},"tempfile.mkdtemp",", ",[13,600,601],{},"tempfile.NamedTemporaryFile"," and any string path containing ",[13,604,605],{},"\u002Ftmp\u002F",". Each converts differently. ",[13,608,609],{},"mkdtemp"," becomes ",[13,612,27],{}," directly. ",[13,615,616],{},"NamedTemporaryFile"," usually becomes a named file inside ",[13,619,27],{},", which also fixes the Windows behaviour where an open ",[13,622,616],{}," cannot be reopened by another handle. Hardcoded paths become ",[13,625,626],{},"tmp_path \u002F \"name\""," and their cleanup code disappears.",[76,629,631],{"className":78,"code":630,"language":80,"meta":81,"style":81},"# Before: manual, leaks on failure, collides under -n\nimport tempfile, shutil\n\ndef test_old():\n    d = tempfile.mkdtemp()\n    try:\n        ...\n    finally:\n        shutil.rmtree(d)\n\n# After\ndef test_new(tmp_path):\n    ...\n",[13,632,633,638,643,647,652,657,662,667,672,677,681,686,691],{"__ignoreMap":81},[85,634,635],{"class":87,"line":88},[85,636,637],{},"# Before: manual, leaks on failure, collides under -n\n",[85,639,640],{"class":87,"line":94},[85,641,642],{},"import tempfile, shutil\n",[85,644,645],{"class":87,"line":101},[85,646,98],{"emptyLinePlaceholder":97},[85,648,649],{"class":87,"line":107},[85,650,651],{},"def test_old():\n",[85,653,654],{"class":87,"line":113},[85,655,656],{},"    d = tempfile.mkdtemp()\n",[85,658,659],{"class":87,"line":119},[85,660,661],{},"    try:\n",[85,663,664],{"class":87,"line":125},[85,665,666],{},"        ...\n",[85,668,669],{"class":87,"line":131},[85,670,671],{},"    finally:\n",[85,673,674],{"class":87,"line":136},[85,675,676],{},"        shutil.rmtree(d)\n",[85,678,679],{"class":87,"line":142},[85,680,98],{"emptyLinePlaceholder":97},[85,682,683],{"class":87,"line":147},[85,684,685],{},"# After\n",[85,687,688],{"class":87,"line":153},[85,689,690],{},"def test_new(tmp_path):\n",[85,692,693],{"class":87,"line":159},[85,694,695],{},"    ...\n",[10,697,698,699,701,702,704,705,707,708,710,711,715,716,719,720,723,724,727],{},"Two things to watch during the migration. A fixture that was module- or session-scoped and used ",[13,700,609],{}," must move to ",[13,703,50],{},", not ",[13,706,27],{},", or collection fails with ",[13,709,178],{},". And code that passed a ",[712,713,714],"em",{},"string"," path into the system under test may now receive a ",[13,717,718],{},"Path","; that is usually an improvement, but a function doing ",[13,721,722],{},"path + \"\u002Fsub\""," will raise, and the fix belongs in the production code rather than in ",[13,725,726],{},"str()"," calls at the call site.",[30,729,731],{"id":730},"working-with-paths-the-code-did-not-expect","Working with paths the code did not expect",[10,733,734,736],{},[13,735,27],{}," hands the test a directory, but the code under test may want something else: a file object, a string path, a path that does not exist yet, or a directory with specific permissions. Each has a short idiom.",[76,738,740],{"className":78,"code":739,"language":80,"meta":81,"style":81},"import os\nimport stat\n\ndef test_missing_file_is_reported(tmp_path):\n    missing = tmp_path \u002F \"nope.toml\"          # a path that deliberately does not exist\n    assert load_config(missing) is None\n\ndef test_unreadable_file_raises(tmp_path):\n    path = tmp_path \u002F \"secret.toml\"\n    path.write_text(\"x = 1\\n\")\n    path.chmod(stat.S_IWUSR)                  # write-only: reading must fail\n    with pytest.raises(PermissionError):\n        load_config(path)\n\ndef test_accepts_a_string_path(tmp_path):\n    path = tmp_path \u002F \"app.toml\"\n    path.write_text(\"x = 1\\n\")\n    assert load_config(str(path)) is not None  # the API accepts str as well as Path\n\ndef test_accepts_an_open_file(tmp_path):\n    path = tmp_path \u002F \"app.toml\"\n    path.write_text(\"x = 1\\n\")\n    with path.open(encoding=\"utf-8\") as fh:\n        assert parse_config(fh) is not None    # separate the reading from the parsing\n",[13,741,742,747,752,756,761,766,771,775,780,785,790,795,800,805,810,816,822,827,833,838,844,849,854,860],{"__ignoreMap":81},[85,743,744],{"class":87,"line":88},[85,745,746],{},"import os\n",[85,748,749],{"class":87,"line":94},[85,750,751],{},"import stat\n",[85,753,754],{"class":87,"line":101},[85,755,98],{"emptyLinePlaceholder":97},[85,757,758],{"class":87,"line":107},[85,759,760],{},"def test_missing_file_is_reported(tmp_path):\n",[85,762,763],{"class":87,"line":113},[85,764,765],{},"    missing = tmp_path \u002F \"nope.toml\"          # a path that deliberately does not exist\n",[85,767,768],{"class":87,"line":119},[85,769,770],{},"    assert load_config(missing) is None\n",[85,772,773],{"class":87,"line":125},[85,774,98],{"emptyLinePlaceholder":97},[85,776,777],{"class":87,"line":131},[85,778,779],{},"def test_unreadable_file_raises(tmp_path):\n",[85,781,782],{"class":87,"line":136},[85,783,784],{},"    path = tmp_path \u002F \"secret.toml\"\n",[85,786,787],{"class":87,"line":142},[85,788,789],{},"    path.write_text(\"x = 1\\n\")\n",[85,791,792],{"class":87,"line":147},[85,793,794],{},"    path.chmod(stat.S_IWUSR)                  # write-only: reading must fail\n",[85,796,797],{"class":87,"line":153},[85,798,799],{},"    with pytest.raises(PermissionError):\n",[85,801,802],{"class":87,"line":159},[85,803,804],{},"        load_config(path)\n",[85,806,808],{"class":87,"line":807},14,[85,809,98],{"emptyLinePlaceholder":97},[85,811,813],{"class":87,"line":812},15,[85,814,815],{},"def test_accepts_a_string_path(tmp_path):\n",[85,817,819],{"class":87,"line":818},16,[85,820,821],{},"    path = tmp_path \u002F \"app.toml\"\n",[85,823,825],{"class":87,"line":824},17,[85,826,789],{},[85,828,830],{"class":87,"line":829},18,[85,831,832],{},"    assert load_config(str(path)) is not None  # the API accepts str as well as Path\n",[85,834,836],{"class":87,"line":835},19,[85,837,98],{"emptyLinePlaceholder":97},[85,839,841],{"class":87,"line":840},20,[85,842,843],{},"def test_accepts_an_open_file(tmp_path):\n",[85,845,847],{"class":87,"line":846},21,[85,848,821],{},[85,850,852],{"class":87,"line":851},22,[85,853,789],{},[85,855,857],{"class":87,"line":856},23,[85,858,859],{},"    with path.open(encoding=\"utf-8\") as fh:\n",[85,861,863],{"class":87,"line":862},24,[85,864,865],{},"        assert parse_config(fh) is not None    # separate the reading from the parsing\n",[10,867,868,869,872],{},"The last example is worth generalising. A function that takes an open file object rather than a path is testable with ",[13,870,871],{},"io.StringIO"," and needs no filesystem at all — which is the cheapest possible test and the strongest argument for splitting \"find and open the file\" from \"parse its contents\".",[10,874,875,876,879,880,883],{},"Permissions deserve one caution: a test running as root ignores the mode bits entirely, so the ",[13,877,878],{},"PermissionError"," case passes locally and silently does nothing in a container that runs as root. Skip such tests when ",[13,881,882],{},"os.geteuid() == 0"," rather than letting them pass vacuously.",[231,885,887,957],{"className":886},[234],[236,888,244,893,244,896,244,899,244,902,244,904,244,908,244,912,244,916,244,921,244,924,244,926,244,930,244,933,244,937,244,939,244,942,244,945,244,948,244,950,244,954],{"viewBox":889,"role":239,"ariaLabelledBy":890,"xmlns":243},"0 0 760 320",[891,892],"pathneeds-t","pathneeds-d",[246,894,895],{"id":891},"Four things code may want instead of a directory",[250,897,898],{"id":892},"A stack of four requirements a test may need to satisfy - a non-existent path, an unreadable file, a string rather than a Path, and an already-open file object - with the idiom for each.",[254,900],{"x":256,"y":256,"width":257,"height":901,"rx":259,"fill":260},"320",[262,903,895],{"x":264,"y":265,"textAnchor":266,"fontSize":267,"fontWeight":268,"fill":269},[254,905],{"x":265,"y":906,"width":268,"height":273,"rx":276,"fill":296,"stroke":907,"strokeWidth":421},"56","#e07a5f",[254,909],{"x":265,"y":906,"width":910,"height":273,"rx":911,"fill":907},"8","4",[262,913,915],{"x":273,"y":914,"fontSize":444,"fontWeight":268,"fill":269},"86","a path that does not exist",[262,917,920],{"x":918,"y":914,"textAnchor":919,"fontSize":430,"fill":269},"714","end","build the path, never create the file",[254,922],{"x":265,"y":923,"width":268,"height":273,"rx":276,"fill":260,"stroke":277,"strokeWidth":421},"120",[254,925],{"x":265,"y":923,"width":910,"height":273,"rx":911,"fill":277},[262,927,929],{"x":273,"y":928,"fontSize":444,"fontWeight":268,"fill":269},"150","an unreadable file",[262,931,932],{"x":918,"y":928,"textAnchor":919,"fontSize":430,"fill":269},"chmod after writing; skip when running as root",[254,934],{"x":265,"y":935,"width":268,"height":273,"rx":276,"fill":296,"stroke":936,"strokeWidth":421},"184","#81b29a",[254,938],{"x":265,"y":935,"width":910,"height":273,"rx":911,"fill":936},[262,940,941],{"x":273,"y":440,"fontSize":444,"fontWeight":268,"fill":269},"a str rather than a Path",[262,943,944],{"x":918,"y":440,"textAnchor":919,"fontSize":430,"fill":269},"str(tmp_path \u002F name) proves the API accepts both",[254,946],{"x":265,"y":947,"width":268,"height":273,"rx":276,"fill":260,"stroke":269,"strokeWidth":421},"248",[254,949],{"x":265,"y":947,"width":910,"height":273,"rx":911,"fill":269},[262,951,953],{"x":273,"y":952,"fontSize":444,"fontWeight":268,"fill":269},"278","an open file object",[262,955,956],{"x":918,"y":952,"textAnchor":919,"fontSize":430,"fill":269},"path.open(), or io.StringIO with no filesystem at all",[352,958,959],{},"The last row is the design hint: a parser that takes a file object needs no temporary directory to test.",[30,961,963],{"id":962},"edge-cases-and-failure-modes","Edge cases and failure modes",[35,965,966,972,980,990,998,1004],{},[38,967,968,971],{},[41,969,970],{},"Long test names are truncated."," Directory names are shortened to fit filesystem limits, so two similarly named parametrized tests get numeric suffixes rather than descriptive paths.",[38,973,974,979],{},[41,975,976,978],{},[13,977,27],{}," is not empty of parents."," It is nested inside a per-run directory, so a test that walks upwards from it will find pytest's own structure.",[38,981,982,985,986,989],{},[41,983,984],{},"Symlinks and case sensitivity follow the real filesystem."," A test that passes on Linux may fail on a case-insensitive macOS volume; ",[13,987,988],{},"pyfakefs"," is one way to normalise that, at the cost of realism.",[38,991,992,997],{},[41,993,994,996],{},[13,995,550],{}," is deleted at the start of the run."," Pointing it at an existing directory destroys the contents; always use a dedicated path.",[38,999,1000,1003],{},[41,1001,1002],{},"Files left open at teardown block removal on Windows."," Close handles explicitly, or the cleanup of the retained directory fails with a permission error several runs later.",[38,1005,1006,1009,1010,1012,1013,1015,1016,1018,1019,1022],{},[41,1007,1008],{},"A session-scoped directory is shared, including its dirt."," Tests that write into a ",[13,1011,50],{}," tree must not assume it is empty; give each test a subdirectory if isolation matters.\nOne consequence of retention worth planning for in CI: ",[13,1014,550],{}," accumulates the files from every test in the run, and a suite that writes large fixtures can produce an artefact bundle of hundreds of megabytes. Either point ",[13,1017,550],{}," at a path that is only collected on failure, or prune it before upload — keeping the directories for the ten failing tests rather than for all four thousand. The retention policy setting handles this natively when the pytest version supports it, and a ",[13,1020,1021],{},"find","-based prune is the portable fallback.",[231,1024,1026,1090],{"className":1025},[234],[236,1027,244,1032,244,1035,244,1038,244,1041,244,1043,244,1045,244,1047,244,1050,244,1053,244,1055,244,1058,244,1061,244,1064,244,1066,244,1069,244,1072,244,1075,244,1077,244,1080,244,1083,244,1086,244,1088],{"viewBox":1028,"role":239,"ariaLabelledBy":1029,"xmlns":243},"0 0 760 230",[1030,1031],"tmpretention-t","tmpretention-d",[246,1033,1034],{"id":1030},"Retention settings and what each keeps",[250,1036,1037],{"id":1031},"A table of three retention policies - all, failed and none - with what each keeps on disk and the artefact size implication for CI.",[254,1039],{"x":256,"y":256,"width":257,"height":1040,"rx":259,"fill":260},"230",[262,1042,1034],{"x":264,"y":265,"textAnchor":266,"fontSize":267,"fontWeight":268,"fill":269},[254,1044],{"x":272,"y":273,"width":274,"height":275,"rx":276,"fill":277,"stroke":269,"strokeWidth":278},[262,1046,284],{"x":281,"y":282,"fontSize":283,"fontWeight":268,"fill":269},[262,1048,1049],{"x":287,"y":282,"textAnchor":266,"fontSize":283,"fontWeight":268,"fill":269},"Keeps",[262,1051,1052],{"x":291,"y":282,"textAnchor":266,"fontSize":283,"fontWeight":268,"fill":269},"CI implication",[254,1054],{"x":272,"y":295,"width":274,"height":275,"fill":296,"stroke":269,"strokeWidth":297},[262,1056,1057],{"x":281,"y":300,"fontSize":301,"fill":269},"policy = all (default)",[262,1059,1060],{"x":287,"y":300,"textAnchor":266,"fontSize":301,"fill":269},"last three runs",[262,1062,1063],{"x":291,"y":300,"textAnchor":266,"fontSize":301,"fill":269},"largest bundle",[254,1065],{"x":272,"y":311,"width":274,"height":275,"fill":260,"stroke":269,"strokeWidth":297},[262,1067,1068],{"x":281,"y":314,"fontSize":301,"fill":269},"policy = failed",[262,1070,1071],{"x":287,"y":314,"textAnchor":266,"fontSize":301,"fill":269},"failing tests only",[262,1073,1074],{"x":291,"y":314,"textAnchor":266,"fontSize":301,"fill":269},"usually what you want",[254,1076],{"x":272,"y":321,"width":274,"height":275,"fill":296,"stroke":269,"strokeWidth":297},[262,1078,1079],{"x":281,"y":324,"fontSize":301,"fill":269},"policy = none",[262,1081,1082],{"x":287,"y":324,"textAnchor":266,"fontSize":301,"fill":269},"nothing",[262,1084,1085],{"x":291,"y":324,"textAnchor":266,"fontSize":301,"fill":269},"no post-mortem possible",[87,1087],{"x1":346,"y1":273,"x2":346,"y2":332,"stroke":269,"strokeWidth":297},[87,1089],{"x1":350,"y1":273,"x2":350,"y2":332,"stroke":269,"strokeWidth":297},[352,1091,1092],{},"The middle row is the right CI default: the files you might read, and none of the ones you will not.",[30,1094,1096],{"id":1095},"frequently-asked-questions","Frequently Asked Questions",[10,1098,1099,1102],{},[41,1100,1101],{},"Does pytest delete tmp_path immediately after the test?","\nNo. pytest keeps the last three test-run directories and removes older ones, so the files from a failed run are still on disk for inspection. The path is printed in the failure output when the fixture is used.",[10,1104,1105,1108,1109,1111],{},[41,1106,1107],{},"Is tmp_path safe under pytest-xdist?","\nYes. The base temporary directory includes the worker id, so two workers never share a path. Hand-rolled paths under ",[13,1110,19],{}," do collide, which is a common cause of parallel-only flakiness.",[10,1113,1114,1117,1119,1120,1122,1123,64],{},[41,1115,1116],{},"What is the difference between tmp_path and tmp_path_factory?",[13,1118,27],{}," is function-scoped and gives one directory per test. ",[13,1121,50],{}," is session-scoped and hands out directories on demand, which is what a session-scoped fixture must use to avoid a ",[13,1124,178],{},[10,1126,1127,1130,1131,1133,1134,1137,1138,1140,1141,1147,1148,1150,1151,1153],{},[41,1128,1129],{},"Can I control where the temporary directories are created?","\nYes, with ",[13,1132,550],{}," on the command line or the ",[13,1135,1136],{},"tmp_path_retention"," settings in the ini file. Use ",[13,1139,550],{}," in CI to place them on a fast volume and to collect them as artefacts.\n",[41,1142,1143,1144,1146],{},"Does ",[13,1145,27],{}," work inside a container with a read-only root filesystem?","\nOnly if the base temporary directory is writable. Point ",[13,1149,550],{}," at a mounted volume or at ",[13,1152,19],{}," when the root filesystem is read-only, and set the pytest cache directory alongside it — both default to locations that a hardened container image forbids writing to.",[30,1155,1157],{"id":1156},"related","Related",[35,1159,1160,1166,1173,1182],{},[38,1161,1162,1165],{},[60,1163,1164],{"href":62},"Faking the filesystem and environment"," — the full set of isolation tools and when each applies.",[38,1167,1168,1172],{},[60,1169,1171],{"href":1170},"\u002Fadvanced-mocking-test-doubles-in-python\u002Ffaking-the-filesystem-and-environment\u002Fpatching-environment-variables-with-monkeypatch-setenv\u002F","Patching environment variables with monkeypatch.setenv"," — the other half of process-state isolation.",[38,1174,1175,1179,1180,64],{},[60,1176,1178],{"href":1177},"\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002Ffixing-scopemismatch-errors-in-pytest\u002F","Fixing ScopeMismatch errors in pytest"," — the error that sends you to ",[13,1181,50],{},[38,1183,1184,1188],{},[60,1185,1187],{"href":1186},"\u002Fadvanced-pytest-architecture-configuration\u002Foptimizing-test-discovery\u002Fpytest-xdist-vs-pytest-parallel-performance-comparison\u002F","pytest-xdist vs pytest-parallel performance comparison"," — why per-worker paths matter.",[10,1190,1191,1192],{},"← Back to ",[60,1193,1194],{"href":62},"Faking the Filesystem and Environment",[1196,1197,1198],"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":81,"searchDepth":94,"depth":94,"links":1200},[1201,1202,1203,1204,1205,1206,1207,1208,1209],{"id":32,"depth":94,"text":33},{"id":67,"depth":94,"text":68},{"id":357,"depth":94,"text":358},{"id":488,"depth":94,"text":489},{"id":587,"depth":94,"text":588},{"id":730,"depth":94,"text":731},{"id":962,"depth":94,"text":963},{"id":1095,"depth":94,"text":1096},{"id":1156,"depth":94,"text":1157},"Replace tempfile with pytest tmp_path: automatic cleanup, per-test uniqueness, xdist safety, tmp_path_factory for shared trees, and how to inspect a failed run's files.","md",{"slug":1213,"type":1214,"breadcrumb":27,"datePublished":1215,"dateModified":1215,"faq":1216,"howto":1225},"using-tmp-path-instead-of-tempfile-in-tests","article","2026-08-01",[1217,1219,1221,1223],{"q":1101,"a":1218},"No. pytest keeps the last three test-run directories and removes older ones, so the files from a failed run are still on disk for inspection. The path is printed in the failure output when the fixture is used.",{"q":1107,"a":1220},"Yes. The base temporary directory includes the worker id, so two workers never share a path. Hand-rolled paths under \u002Ftmp do collide, which is a common cause of parallel-only flakiness.",{"q":1116,"a":1222},"tmp_path is function-scoped and gives one directory per test. tmp_path_factory is session-scoped and hands out directories on demand, which is what a session-scoped fixture must use to avoid a ScopeMismatch.",{"q":1129,"a":1224},"Yes, with --basetemp on the command line or the tmp_path_retention settings in the ini file. Use --basetemp in CI to place them on a fast volume and to collect them as artefacts.",{"name":1226,"description":1227,"steps":1228},"How to use tmp_path instead of tempfile","Get isolated, inspectable, parallel-safe temporary files without manual cleanup.",[1229,1232,1235,1238],{"name":1230,"text":1231},"Request tmp_path in the test","Take the fixture as an argument and treat it as a pathlib.Path to an empty directory.",{"name":1233,"text":1234},"Build the tree with pathlib","Create files and subdirectories relative to tmp_path rather than with string paths.",{"name":1236,"text":1237},"Use tmp_path_factory for shared trees","Request the session-scoped factory when a fixture wider than function scope needs a directory.",{"name":1239,"text":1240},"Inspect the retained directory after a failure","Read the path from the failure output and examine the files pytest kept.","\u002Fadvanced-mocking-test-doubles-in-python\u002Ffaking-the-filesystem-and-environment\u002Fusing-tmp-path-instead-of-tempfile-in-tests",{"title":5,"description":1210},"advanced-mocking-test-doubles-in-python\u002Ffaking-the-filesystem-and-environment\u002Fusing-tmp-path-instead-of-tempfile-in-tests\u002Findex","7QdxCgS5EOgxNSlpD0QD9A6mwN6SEj4IpZNqUn3Rh2w",1785613404446]