[{"data":1,"prerenderedAt":1303},["ShallowReactive",2],{"page-\u002Fadvanced-mocking-test-doubles-in-python\u002Ffaking-the-filesystem-and-environment\u002Fpatching-environment-variables-with-monkeypatch-setenv\u002F":3},{"id":4,"title":5,"body":6,"description":1270,"extension":1271,"meta":1272,"navigation":101,"path":1299,"seo":1300,"stem":1301,"__hash__":1302},"content\u002Fadvanced-mocking-test-doubles-in-python\u002Ffaking-the-filesystem-and-environment\u002Fpatching-environment-variables-with-monkeypatch-setenv\u002Findex.md","Patching Environment Variables with monkeypatch.setenv",{"type":7,"value":8,"toc":1258},"minimark",[9,18,23,47,51,61,126,144,147,225,228,360,364,387,490,494,500,543,549,614,622,626,639,646,694,705,709,715,766,769,773,776,866,869,889,895,910,980,984,1099,1169,1173,1182,1191,1213,1217,1248,1254],[10,11,12,13,17],"p",{},"A test sets ",[14,15,16],"code",{},"os.environ[\"APP_REGION\"] = \"eu-west-1\"",", passes, and leaves the variable set for every test that follows. Another test reads a variable the developer's shell happens to define and passes locally but fails in CI. Both are the same bug: the environment is process-global state, and pytest gives you exactly one tool that treats it as such.",[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+"," for the ",[14,35,36],{},"monkeypatch"," fixture.",[27,39,40,41,46],{},"The broader isolation picture in ",[42,43,45],"a",{"href":44},"\u002Fadvanced-mocking-test-doubles-in-python\u002Ffaking-the-filesystem-and-environment\u002F","faking the filesystem and environment",".",[19,48,50],{"id":49},"solution","Solution",[10,52,53,56,57,60],{},[14,54,55],{},"monkeypatch.setenv"," and ",[14,58,59],{},"monkeypatch.delenv"," record the prior state and restore it at teardown, including when the test raises:",[62,63,68],"pre",{"className":64,"code":65,"language":66,"meta":67,"style":67},"language-python shiki shiki-themes github-light github-dark","def test_client_uses_the_configured_region(monkeypatch):\n    monkeypatch.setenv(\"APP_REGION\", \"eu-west-1\")\n    monkeypatch.setenv(\"APP_TIMEOUT\", \"5\")\n    monkeypatch.delenv(\"APP_DEBUG\", raising=False)     # absent is a valid state\n\n    client = build_client()\n\n    assert client.region == \"eu-west-1\"\n    assert client.timeout == 5\n","python","",[14,69,70,78,84,90,96,103,109,114,120],{"__ignoreMap":67},[71,72,75],"span",{"class":73,"line":74},"line",1,[71,76,77],{},"def test_client_uses_the_configured_region(monkeypatch):\n",[71,79,81],{"class":73,"line":80},2,[71,82,83],{},"    monkeypatch.setenv(\"APP_REGION\", \"eu-west-1\")\n",[71,85,87],{"class":73,"line":86},3,[71,88,89],{},"    monkeypatch.setenv(\"APP_TIMEOUT\", \"5\")\n",[71,91,93],{"class":73,"line":92},4,[71,94,95],{},"    monkeypatch.delenv(\"APP_DEBUG\", raising=False)     # absent is a valid state\n",[71,97,99],{"class":73,"line":98},5,[71,100,102],{"emptyLinePlaceholder":101},true,"\n",[71,104,106],{"class":73,"line":105},6,[71,107,108],{},"    client = build_client()\n",[71,110,112],{"class":73,"line":111},7,[71,113,102],{"emptyLinePlaceholder":101},[71,115,117],{"class":73,"line":116},8,[71,118,119],{},"    assert client.region == \"eu-west-1\"\n",[71,121,123],{"class":73,"line":122},9,[71,124,125],{},"    assert client.timeout == 5\n",[10,127,128,131,132,136,137,140,141,46],{},[14,129,130],{},"raising=False"," matters more than it looks: a test that must run with a variable ",[133,134,135],"em",{},"absent"," cannot assume it was present, and the default ",[14,138,139],{},"raising=True"," turns a clean environment into a ",[14,142,143],{},"KeyError",[10,145,146],{},"For a set of variables shared by many tests, an autouse fixture establishes a known baseline so no test inherits the developer's shell:",[62,148,150],{"className":64,"code":149,"language":66,"meta":67,"style":67},"import pytest\n\nBASELINE = {\n    \"APP_REGION\": \"eu-west-1\",\n    \"APP_TIMEOUT\": \"5\",\n    \"TZ\": \"UTC\",\n}\n\n@pytest.fixture(autouse=True)\ndef _env_baseline(monkeypatch):\n    for name in (\"APP_DEBUG\", \"APP_PROFILE\", \"AWS_PROFILE\"):\n        monkeypatch.delenv(name, raising=False)        # nothing inherited\n    for name, value in BASELINE.items():\n        monkeypatch.setenv(name, value)\n",[14,151,152,157,161,166,171,176,181,186,190,195,201,207,213,219],{"__ignoreMap":67},[71,153,154],{"class":73,"line":74},[71,155,156],{},"import pytest\n",[71,158,159],{"class":73,"line":80},[71,160,102],{"emptyLinePlaceholder":101},[71,162,163],{"class":73,"line":86},[71,164,165],{},"BASELINE = {\n",[71,167,168],{"class":73,"line":92},[71,169,170],{},"    \"APP_REGION\": \"eu-west-1\",\n",[71,172,173],{"class":73,"line":98},[71,174,175],{},"    \"APP_TIMEOUT\": \"5\",\n",[71,177,178],{"class":73,"line":105},[71,179,180],{},"    \"TZ\": \"UTC\",\n",[71,182,183],{"class":73,"line":111},[71,184,185],{},"}\n",[71,187,188],{"class":73,"line":116},[71,189,102],{"emptyLinePlaceholder":101},[71,191,192],{"class":73,"line":122},[71,193,194],{},"@pytest.fixture(autouse=True)\n",[71,196,198],{"class":73,"line":197},10,[71,199,200],{},"def _env_baseline(monkeypatch):\n",[71,202,204],{"class":73,"line":203},11,[71,205,206],{},"    for name in (\"APP_DEBUG\", \"APP_PROFILE\", \"AWS_PROFILE\"):\n",[71,208,210],{"class":73,"line":209},12,[71,211,212],{},"        monkeypatch.delenv(name, raising=False)        # nothing inherited\n",[71,214,216],{"class":73,"line":215},13,[71,217,218],{},"    for name, value in BASELINE.items():\n",[71,220,222],{"class":73,"line":221},14,[71,223,224],{},"        monkeypatch.setenv(name, value)\n",[10,226,227],{},"The delete list is the half people omit, and it is the half that makes a suite reproducible on a machine that is not yours.",[229,230,233,356],"figure",{"className":231},[232],"diagram",[234,235,242,243,242,247,242,251,242,269,242,277,242,286,242,295,242,301,242,306,242,312,242,315,242,319,242,322,242,326,242,329,242,333,242,336,242,340,242,343,242,347,242,350],"svg",{"viewBox":236,"role":237,"ariaLabelledBy":238,"xmlns":241},"0 0 760 172","img",[239,240],"envrestore-t","envrestore-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[244,245,246],"title",{"id":239},"What monkeypatch records and restores",[248,249,250],"desc",{"id":240},"A left-to-right sequence: monkeypatch records the previous value or its absence, applies the new value, the test body runs, and teardown restores exactly the previous state whether the test passed or raised.",[252,253,254,255,242],"defs",{},"\n    ",[256,257,264],"marker",{"id":258,"viewBox":259,"refX":260,"refY":261,"markerWidth":262,"markerHeight":262,"orient":263},"envrestore-a","0 0 10 10","9","5","7","auto-start-reverse",[265,266],"path",{"d":267,"fill":268},"M0 0 L10 5 L0 10 z","#81b29a",[270,271],"rect",{"x":272,"y":272,"width":273,"height":274,"rx":275,"fill":276},"0","760","172","14","#fffdf8",[278,279,246],"text",{"x":280,"y":281,"textAnchor":282,"fontSize":283,"fontWeight":284,"fill":285},"380","30","middle","15","700","#3d405b",[270,287],{"x":288,"y":289,"width":290,"height":291,"rx":292,"fill":293,"stroke":268,"strokeWidth":294},"26","62","142","76","12","#f4f1de","1.8",[278,296,300],{"x":297,"y":298,"textAnchor":282,"fontSize":299,"fontWeight":284,"fill":285},"97","96","12.5","record prior state",[278,302,305],{"x":297,"y":303,"textAnchor":282,"fontSize":304,"fill":285},"113","11","value or absence",[73,307],{"x1":308,"y1":309,"x2":310,"y2":309,"stroke":268,"strokeWidth":294,"markerEnd":311},"176","100","208","url(#envrestore-a)",[270,313],{"x":314,"y":289,"width":290,"height":291,"rx":292,"fill":276,"stroke":268,"strokeWidth":294},"214",[278,316,318],{"x":317,"y":298,"textAnchor":282,"fontSize":299,"fontWeight":284,"fill":285},"286","apply the change",[278,320,321],{"x":317,"y":303,"textAnchor":282,"fontSize":304,"fill":285},"setenv or delenv",[73,323],{"x1":324,"y1":309,"x2":325,"y2":309,"stroke":268,"strokeWidth":294,"markerEnd":311},"364","396",[270,327],{"x":328,"y":289,"width":290,"height":291,"rx":292,"fill":293,"stroke":268,"strokeWidth":294},"403",[278,330,332],{"x":331,"y":298,"textAnchor":282,"fontSize":299,"fontWeight":284,"fill":285},"474","run the test",[278,334,335],{"x":331,"y":303,"textAnchor":282,"fontSize":304,"fill":285},"may raise",[73,337],{"x1":338,"y1":309,"x2":339,"y2":309,"stroke":268,"strokeWidth":294,"markerEnd":311},"552","584",[270,341],{"x":342,"y":289,"width":290,"height":291,"rx":292,"fill":276,"stroke":268,"strokeWidth":294},"592",[278,344,346],{"x":345,"y":298,"textAnchor":282,"fontSize":299,"fontWeight":284,"fill":285},"663","restore",[278,348,349],{"x":345,"y":303,"textAnchor":282,"fontSize":304,"fill":285},"always, in teardown",[278,351,355],{"x":280,"y":352,"textAnchor":282,"fontSize":353,"fontStyle":354,"fill":285},"164","11.5","italic","A plain os.environ assignment has no step one and no step four.",[357,358,359],"figcaption",{},"The recording step is why monkeypatch is safe where a bare assignment is not: absence is a state it can restore to.",[19,361,363],{"id":362},"why-this-works","Why this works",[10,365,366,368,369,372,373,376,377,376,380,56,383,386],{},[14,367,36],{}," maintains an undo stack. Each ",[14,370,371],{},"setenv"," pushes the previous value — or a sentinel meaning \"was not set\" — and the fixture's finalizer pops the stack in reverse order during teardown, which pytest runs even when the test fails. Because the stack is per-test and the fixture is function-scoped, isolation is automatic: no test can observe another's changes, and no ordering assumption is required. The same mechanism backs ",[14,374,375],{},"setattr",", ",[14,378,379],{},"delattr",[14,381,382],{},"chdir",[14,384,385],{},"syspath_prepend",", which is why those should be preferred over their manual equivalents for exactly the same reason.",[229,388,390,487],{"className":389},[232],[234,391,242,396,242,399,242,402,242,405,242,407,242,416,242,420,242,424,242,428,242,432,242,436,242,439,242,442,242,445,242,449,242,452,242,455,242,457,242,461,242,464,242,467,242,470,242,474,242,477,242,480,242,484],{"viewBox":392,"role":237,"ariaLabelledBy":393,"xmlns":241},"0 0 760 270",[394,395],"envmethods-t","envmethods-d",[244,397,398],{"id":394},"The monkeypatch methods for process state",[248,400,401],{"id":395},"A table of four monkeypatch methods - setenv, delenv, chdir and syspath_prepend - stating what each changes and what it restores at teardown.",[270,403],{"x":272,"y":272,"width":273,"height":404,"rx":275,"fill":276},"270",[278,406,398],{"x":280,"y":281,"textAnchor":282,"fontSize":283,"fontWeight":284,"fill":285},[270,408],{"x":409,"y":410,"width":411,"height":412,"rx":413,"fill":414,"stroke":285,"strokeWidth":415},"24","52","712","40","10","#f2cc8f","1.5",[278,417,419],{"x":418,"y":291,"fontSize":292,"fontWeight":284,"fill":285},"38","Criterion",[278,421,423],{"x":422,"y":291,"textAnchor":282,"fontSize":292,"fontWeight":284,"fill":285},"390","Changes",[278,425,427],{"x":426,"y":291,"textAnchor":282,"fontSize":292,"fontWeight":284,"fill":285},"620","Restores",[270,429],{"x":409,"y":430,"width":411,"height":412,"fill":293,"stroke":285,"strokeWidth":431},"92","1",[278,433,435],{"x":418,"y":434,"fontSize":353,"fill":285},"116","setenv(name, value)",[278,437,438],{"x":422,"y":434,"textAnchor":282,"fontSize":353,"fill":285},"one variable",[278,440,441],{"x":426,"y":434,"textAnchor":282,"fontSize":353,"fill":285},"the prior value or absence",[270,443],{"x":409,"y":444,"width":411,"height":412,"fill":276,"stroke":285,"strokeWidth":431},"132",[278,446,448],{"x":418,"y":447,"fontSize":353,"fill":285},"156","delenv(name, raising=False)",[278,450,451],{"x":422,"y":447,"textAnchor":282,"fontSize":353,"fill":285},"removes a variable",[278,453,454],{"x":426,"y":447,"textAnchor":282,"fontSize":353,"fill":285},"the prior value",[270,456],{"x":409,"y":274,"width":411,"height":412,"fill":293,"stroke":285,"strokeWidth":431},[278,458,460],{"x":418,"y":459,"fontSize":353,"fill":285},"196","chdir(path)",[278,462,463],{"x":422,"y":459,"textAnchor":282,"fontSize":353,"fill":285},"the working directory",[278,465,466],{"x":426,"y":459,"textAnchor":282,"fontSize":353,"fill":285},"the original directory",[270,468],{"x":409,"y":469,"width":411,"height":412,"fill":276,"stroke":285,"strokeWidth":431},"212",[278,471,473],{"x":418,"y":472,"fontSize":353,"fill":285},"236","syspath_prepend(path)",[278,475,476],{"x":422,"y":472,"textAnchor":282,"fontSize":353,"fill":285},"sys.path[0]",[278,478,479],{"x":426,"y":472,"textAnchor":282,"fontSize":353,"fill":285},"the original sys.path",[73,481],{"x1":482,"y1":410,"x2":482,"y2":483,"stroke":285,"strokeWidth":431},"274","252",[73,485],{"x1":486,"y1":410,"x2":486,"y2":483,"stroke":285,"strokeWidth":431},"505",[357,488,489],{},"All four share one undo stack, so a single fixture can change several kinds of process state and rely on complete restoration.",[19,491,493],{"id":492},"when-the-variable-is-read-at-import","When the variable is read at import",[10,495,496,497,499],{},"The most common report — \"setenv does nothing\" — is not about ",[14,498,371],{}," at all. It is about when the value was read.",[62,501,503],{"className":64,"code":502,"language":66,"meta":67,"style":67},"# config.py — read once, at import\nimport os\nREGION = os.environ.get(\"APP_REGION\", \"us-east-1\")     # frozen at import time\n\n# The test sets the variable, but REGION was bound before the test ran.\ndef test_region(monkeypatch):\n    monkeypatch.setenv(\"APP_REGION\", \"eu-west-1\")\n    assert config.REGION == \"eu-west-1\"                # FAILS: still us-east-1\n",[14,504,505,510,515,520,524,529,534,538],{"__ignoreMap":67},[71,506,507],{"class":73,"line":74},[71,508,509],{},"# config.py — read once, at import\n",[71,511,512],{"class":73,"line":80},[71,513,514],{},"import os\n",[71,516,517],{"class":73,"line":86},[71,518,519],{},"REGION = os.environ.get(\"APP_REGION\", \"us-east-1\")     # frozen at import time\n",[71,521,522],{"class":73,"line":92},[71,523,102],{"emptyLinePlaceholder":101},[71,525,526],{"class":73,"line":98},[71,527,528],{},"# The test sets the variable, but REGION was bound before the test ran.\n",[71,530,531],{"class":73,"line":105},[71,532,533],{},"def test_region(monkeypatch):\n",[71,535,536],{"class":73,"line":111},[71,537,83],{},[71,539,540],{"class":73,"line":116},[71,541,542],{},"    assert config.REGION == \"eu-west-1\"                # FAILS: still us-east-1\n",[10,544,545,546,46],{},"Three fixes, in order of preference. Read the variable inside a function so every call sees the current environment; cache it behind an accessor that tests can clear; or, as a last resort, patch the already-bound constant with ",[14,547,548],{},"monkeypatch.setattr(\"config.REGION\", \"eu-west-1\")",[62,550,552],{"className":64,"code":551,"language":66,"meta":67,"style":67},"# config.py — read on use, cached\nfrom functools import cache\nimport os\n\n@cache\ndef region() -> str:\n    return os.environ.get(\"APP_REGION\", \"us-east-1\")\n\n# The test clears the cache, then the accessor sees the patched environment.\ndef test_region(monkeypatch):\n    monkeypatch.setenv(\"APP_REGION\", \"eu-west-1\")\n    config.region.cache_clear()\n    assert config.region() == \"eu-west-1\"\n",[14,553,554,559,564,568,572,577,582,587,591,596,600,604,609],{"__ignoreMap":67},[71,555,556],{"class":73,"line":74},[71,557,558],{},"# config.py — read on use, cached\n",[71,560,561],{"class":73,"line":80},[71,562,563],{},"from functools import cache\n",[71,565,566],{"class":73,"line":86},[71,567,514],{},[71,569,570],{"class":73,"line":92},[71,571,102],{"emptyLinePlaceholder":101},[71,573,574],{"class":73,"line":98},[71,575,576],{},"@cache\n",[71,578,579],{"class":73,"line":105},[71,580,581],{},"def region() -> str:\n",[71,583,584],{"class":73,"line":111},[71,585,586],{},"    return os.environ.get(\"APP_REGION\", \"us-east-1\")\n",[71,588,589],{"class":73,"line":116},[71,590,102],{"emptyLinePlaceholder":101},[71,592,593],{"class":73,"line":122},[71,594,595],{},"# The test clears the cache, then the accessor sees the patched environment.\n",[71,597,598],{"class":73,"line":197},[71,599,533],{},[71,601,602],{"class":73,"line":203},[71,603,83],{},[71,605,606],{"class":73,"line":209},[71,607,608],{},"    config.region.cache_clear()\n",[71,610,611],{"class":73,"line":215},[71,612,613],{},"    assert config.region() == \"eu-west-1\"\n",[10,615,616,617,621],{},"This is the environment-variable instance of the general problem covered in ",[42,618,620],{"href":619},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fdependency-injection-for-testability\u002Freplacing-singletons-and-module-globals-in-tests\u002F","replacing singletons and module globals in tests",": a value captured at import cannot be changed by patching its source afterwards.",[19,623,625],{"id":624},"subprocesses-workers-and-inheritance","Subprocesses, workers and inheritance",[10,627,628,631,632,635,636,638],{},[14,629,630],{},"os.environ"," is copied into a child at spawn time, so a subprocess started ",[133,633,634],{},"after"," ",[14,637,371],{}," sees the patched value and one started before does not. That makes the ordering inside a test significant when the code under test manages a long-lived child.",[10,640,641,642,645],{},"Under ",[14,643,644],{},"pytest-xdist"," each worker is a separate process with its own copy of the environment, inherited from the controller at startup. A variable set by a test in one worker is invisible to the others, which is usually what you want — but it also means a variable exported by a session-scoped fixture must be set in every worker, not once in the controller.",[62,647,649],{"className":64,"code":648,"language":66,"meta":67,"style":67},"import os, subprocess\n\ndef test_child_sees_the_patched_value(monkeypatch):\n    monkeypatch.setenv(\"APP_REGION\", \"eu-west-1\")\n    out = subprocess.run(\n        [\"python\", \"-c\", \"import os; print(os.environ['APP_REGION'])\"],\n        capture_output=True, text=True, check=True,\n    )\n    assert out.stdout.strip() == \"eu-west-1\"      # inherited at spawn time\n",[14,650,651,656,660,665,669,674,679,684,689],{"__ignoreMap":67},[71,652,653],{"class":73,"line":74},[71,654,655],{},"import os, subprocess\n",[71,657,658],{"class":73,"line":80},[71,659,102],{"emptyLinePlaceholder":101},[71,661,662],{"class":73,"line":86},[71,663,664],{},"def test_child_sees_the_patched_value(monkeypatch):\n",[71,666,667],{"class":73,"line":92},[71,668,83],{},[71,670,671],{"class":73,"line":98},[71,672,673],{},"    out = subprocess.run(\n",[71,675,676],{"class":73,"line":105},[71,677,678],{},"        [\"python\", \"-c\", \"import os; print(os.environ['APP_REGION'])\"],\n",[71,680,681],{"class":73,"line":111},[71,682,683],{},"        capture_output=True, text=True, check=True,\n",[71,685,686],{"class":73,"line":116},[71,687,688],{},"    )\n",[71,690,691],{"class":73,"line":122},[71,692,693],{},"    assert out.stdout.strip() == \"eu-west-1\"      # inherited at spawn time\n",[10,695,696,697,700,701,704],{},"For variables that must reach a child but should not be visible to the parent's own code, pass an explicit ",[14,698,699],{},"env="," mapping to ",[14,702,703],{},"subprocess.run"," instead of mutating the environment at all — a smaller blast radius and a clearer test.",[19,706,708],{"id":707},"verification","Verification",[10,710,711,712,714],{},"A teardown assertion turns any bypass of ",[14,713,36],{}," into an immediate, attributable failure:",[62,716,718],{"className":64,"code":717,"language":66,"meta":67,"style":67},"import os\nimport pytest\n\n@pytest.fixture(autouse=True)\ndef _no_env_leak(request):\n    before = dict(os.environ)\n    yield\n    changed = {k for k in set(before) | set(os.environ)\n               if before.get(k) != os.environ.get(k)}\n    assert not changed, f\"{request.node.nodeid} leaked env vars: {sorted(changed)}\"\n",[14,719,720,724,728,732,736,741,746,751,756,761],{"__ignoreMap":67},[71,721,722],{"class":73,"line":74},[71,723,514],{},[71,725,726],{"class":73,"line":80},[71,727,156],{},[71,729,730],{"class":73,"line":86},[71,731,102],{"emptyLinePlaceholder":101},[71,733,734],{"class":73,"line":92},[71,735,194],{},[71,737,738],{"class":73,"line":98},[71,739,740],{},"def _no_env_leak(request):\n",[71,742,743],{"class":73,"line":105},[71,744,745],{},"    before = dict(os.environ)\n",[71,747,748],{"class":73,"line":111},[71,749,750],{},"    yield\n",[71,752,753],{"class":73,"line":116},[71,754,755],{},"    changed = {k for k in set(before) | set(os.environ)\n",[71,757,758],{"class":73,"line":122},[71,759,760],{},"               if before.get(k) != os.environ.get(k)}\n",[71,762,763],{"class":73,"line":197},[71,764,765],{},"    assert not changed, f\"{request.node.nodeid} leaked env vars: {sorted(changed)}\"\n",[10,767,768],{},"Run the suite in a randomised order once the assertion is in place; order-dependence caused by environment leakage surfaces immediately rather than in the pull request that happens to add a test in the middle.",[19,770,772],{"id":771},"making-configuration-testable-rather-than-patchable","Making configuration testable rather than patchable",[10,774,775],{},"Every technique on this page is a workaround for configuration that reads the environment at a point the test cannot reach. The durable fix is a settings object constructed once, from an explicit mapping, and passed to whatever needs it.",[62,777,779],{"className":64,"code":778,"language":66,"meta":67,"style":67},"from dataclasses import dataclass\nimport os\n\n@dataclass(frozen=True)\nclass Settings:\n    region: str\n    timeout: float\n    debug: bool\n\n    @classmethod\n    def from_env(cls, env: dict[str, str] | None = None) -> \"Settings\":\n        env = os.environ if env is None else env      # the seam: injectable\n        return cls(\n            region=env.get(\"APP_REGION\", \"us-east-1\"),\n            timeout=float(env.get(\"APP_TIMEOUT\", \"5\")),\n            debug=env.get(\"APP_DEBUG\") == \"1\",\n        )\n",[14,780,781,786,790,794,799,804,809,814,819,823,828,833,838,843,848,854,860],{"__ignoreMap":67},[71,782,783],{"class":73,"line":74},[71,784,785],{},"from dataclasses import dataclass\n",[71,787,788],{"class":73,"line":80},[71,789,514],{},[71,791,792],{"class":73,"line":86},[71,793,102],{"emptyLinePlaceholder":101},[71,795,796],{"class":73,"line":92},[71,797,798],{},"@dataclass(frozen=True)\n",[71,800,801],{"class":73,"line":98},[71,802,803],{},"class Settings:\n",[71,805,806],{"class":73,"line":105},[71,807,808],{},"    region: str\n",[71,810,811],{"class":73,"line":111},[71,812,813],{},"    timeout: float\n",[71,815,816],{"class":73,"line":116},[71,817,818],{},"    debug: bool\n",[71,820,821],{"class":73,"line":122},[71,822,102],{"emptyLinePlaceholder":101},[71,824,825],{"class":73,"line":197},[71,826,827],{},"    @classmethod\n",[71,829,830],{"class":73,"line":203},[71,831,832],{},"    def from_env(cls, env: dict[str, str] | None = None) -> \"Settings\":\n",[71,834,835],{"class":73,"line":209},[71,836,837],{},"        env = os.environ if env is None else env      # the seam: injectable\n",[71,839,840],{"class":73,"line":215},[71,841,842],{},"        return cls(\n",[71,844,845],{"class":73,"line":221},[71,846,847],{},"            region=env.get(\"APP_REGION\", \"us-east-1\"),\n",[71,849,851],{"class":73,"line":850},15,[71,852,853],{},"            timeout=float(env.get(\"APP_TIMEOUT\", \"5\")),\n",[71,855,857],{"class":73,"line":856},16,[71,858,859],{},"            debug=env.get(\"APP_DEBUG\") == \"1\",\n",[71,861,863],{"class":73,"line":862},17,[71,864,865],{},"        )\n",[10,867,868],{},"A test then builds settings directly, with no patching at all:",[62,870,872],{"className":64,"code":871,"language":66,"meta":67,"style":67},"def test_client_timeout_is_configurable():\n    settings = Settings.from_env({\"APP_TIMEOUT\": \"0.5\"})\n    assert build_client(settings).timeout == 0.5\n",[14,873,874,879,884],{"__ignoreMap":67},[71,875,876],{"class":73,"line":74},[71,877,878],{},"def test_client_timeout_is_configurable():\n",[71,880,881],{"class":73,"line":80},[71,882,883],{},"    settings = Settings.from_env({\"APP_TIMEOUT\": \"0.5\"})\n",[71,885,886],{"class":73,"line":86},[71,887,888],{},"    assert build_client(settings).timeout == 0.5\n",[10,890,891,892,894],{},"Three properties make this better than ",[14,893,36],{}," for anything beyond a one-off. The dependency is visible in the signature rather than ambient. The parsing — string to float, string to bool — is tested directly, which matters because environment values are always strings and the conversion is where the bugs are. And the object is frozen, so a test cannot accidentally mutate configuration that another test then observes.",[10,896,897,899,900,376,903,376,906,909],{},[14,898,55],{}," remains the right tool for code you do not own, for the process-level variables (",[14,901,902],{},"TZ",[14,904,905],{},"LC_ALL",[14,907,908],{},"PYTHONHASHSEED",") that libraries read directly, and for integration tests that exercise the real startup path. For your own application configuration, injecting a settings object removes the question entirely.",[229,911,913,977],{"className":912},[232],[234,914,242,919,242,922,242,925,242,932,242,935,242,937,242,939,242,942,242,945,242,948,242,950,242,953,242,956,242,958,242,960,242,963,242,966,242,968,242,970,242,973],{"viewBox":915,"role":237,"ariaLabelledBy":916,"xmlns":241},"0 0 760 154",[917,918],"settingseam-t","settingseam-d",[244,920,921],{"id":917},"From ambient environment to injected settings",[248,923,924],{"id":918},"A left-to-right progression: code reading os.environ directly at call sites, then a settings object built from the environment, then a settings object built from an injected mapping, and finally settings passed to every consumer.",[252,926,254,927,242],{},[256,928,930],{"id":929,"viewBox":259,"refX":260,"refY":261,"markerWidth":262,"markerHeight":262,"orient":263},"settingseam-a",[265,931],{"d":267,"fill":268},[270,933],{"x":272,"y":272,"width":273,"height":934,"rx":275,"fill":276},"154",[278,936,921],{"x":280,"y":281,"textAnchor":282,"fontSize":283,"fontWeight":284,"fill":285},[270,938],{"x":288,"y":289,"width":290,"height":291,"rx":292,"fill":293,"stroke":268,"strokeWidth":294},[278,940,941],{"x":297,"y":298,"textAnchor":282,"fontSize":299,"fontWeight":284,"fill":285},"os.environ inline",[278,943,944],{"x":297,"y":303,"textAnchor":282,"fontSize":304,"fill":285},"ambient everywhere",[73,946],{"x1":308,"y1":309,"x2":310,"y2":309,"stroke":268,"strokeWidth":294,"markerEnd":947},"url(#settingseam-a)",[270,949],{"x":314,"y":289,"width":290,"height":291,"rx":292,"fill":276,"stroke":268,"strokeWidth":294},[278,951,952],{"x":317,"y":298,"textAnchor":282,"fontSize":299,"fontWeight":284,"fill":285},"settings from env",[278,954,955],{"x":317,"y":303,"textAnchor":282,"fontSize":304,"fill":285},"one read point",[73,957],{"x1":324,"y1":309,"x2":325,"y2":309,"stroke":268,"strokeWidth":294,"markerEnd":947},[270,959],{"x":328,"y":289,"width":290,"height":291,"rx":292,"fill":293,"stroke":268,"strokeWidth":294},[278,961,962],{"x":331,"y":298,"textAnchor":282,"fontSize":299,"fontWeight":284,"fill":285},"injectable mapping",[278,964,965],{"x":331,"y":303,"textAnchor":282,"fontSize":304,"fill":285},"tests pass a dict",[73,967],{"x1":338,"y1":309,"x2":339,"y2":309,"stroke":268,"strokeWidth":294,"markerEnd":947},[270,969],{"x":342,"y":289,"width":290,"height":291,"rx":292,"fill":276,"stroke":268,"strokeWidth":294},[278,971,972],{"x":345,"y":298,"textAnchor":282,"fontSize":353,"fontWeight":284,"fill":285},"passed to consumers",[278,974,976],{"x":345,"y":975,"textAnchor":282,"fontSize":304,"fill":285},"112","no patching at all",[357,978,979],{},"Each step reduces the number of places the environment is read, and the last one removes the need for monkeypatch in application tests entirely.",[19,981,983],{"id":982},"edge-cases-and-failure-modes","Edge cases and failure modes",[24,985,986,1007,1028,1044,1053,1073],{},[27,987,988,993,994,996,997,1000,1001,1003,1004,46],{},[30,989,990,992],{},[14,991,36],{}," is function-scoped."," A session-scoped fixture cannot request it; use ",[14,995,630],{}," with an explicit ",[14,998,999],{},"try\u002Ffinally",", or the ",[14,1002,36],{}," session workaround via ",[14,1005,1006],{},"MonkeyPatch.context()",[27,1008,1009,635,1012,1015,1016,1019,1020,1023,1024,1027],{},[30,1010,1011],{},"Values must be strings.",[14,1013,1014],{},"setenv(\"PORT\", 8080)"," raises ",[14,1017,1018],{},"TypeError","; pass ",[14,1021,1022],{},"str(8080)",", or use ",[14,1025,1026],{},"prepend="," for path-like variables.",[27,1029,1030,635,1033,1035,1036,1039,1040,1043],{},[30,1031,1032],{},"C-level caches read some variables once.",[14,1034,902],{}," needs ",[14,1037,1038],{},"time.tzset()"," after the change on POSIX, and locale settings need an explicit ",[14,1041,1042],{},"locale.setlocale"," call.",[27,1045,1046,1052],{},[30,1047,1048,1051],{},[14,1049,1050],{},".env"," files loaded by a library at import"," reintroduce the import-time problem, and often override what the test set; disable the loader in the test environment.",[27,1054,1055,1066,1067,1070,1071,46],{},[30,1056,1057,1058,1061,1062,1065],{},"Deleting ",[14,1059,1060],{},"PATH"," or ",[14,1063,1064],{},"HOME"," breaks subprocesses"," in ways that look unrelated — a ",[14,1068,1069],{},"git"," call failing with a cryptic error is usually a missing ",[14,1072,1064],{},[27,1074,1075,1078,1079,376,1082,56,1085,1088,1089,376,1092,56,1095,1098],{},[30,1076,1077],{},"Secrets set in a test can leak into logs."," pytest captures stdout, and a failing test prints it; keep real credentials out of the test environment entirely.\nOne more variable class deserves explicit handling: proxy settings. ",[14,1080,1081],{},"HTTP_PROXY",[14,1083,1084],{},"HTTPS_PROXY",[14,1086,1087],{},"NO_PROXY"," are read by ",[14,1090,1091],{},"requests",[14,1093,1094],{},"httpx",[14,1096,1097],{},"urllib"," directly from the environment, so a developer running behind a corporate proxy gets different network behaviour from CI without either environment mentioning it in configuration. Clearing all three in the baseline fixture removes a category of 'works for me' that is otherwise very hard to attribute, because the symptom is a connection error rather than anything mentioning proxies.",[229,1100,1102,1166],{"className":1101},[232],[234,1103,242,1107,242,1110,242,1113,242,1120,242,1122,242,1124,242,1126,242,1129,242,1132,242,1135,242,1137,242,1140,242,1143,242,1145,242,1147,242,1150,242,1153,242,1155,242,1157,242,1160,242,1163],{"viewBox":236,"role":237,"ariaLabelledBy":1104,"xmlns":241},[1105,1106],"envsources-t","envsources-d",[244,1108,1109],{"id":1105},"Where an environment value can come from",[248,1111,1112],{"id":1106},"A left-to-right chain of environment value sources: the developer shell or CI runner, a dotenv file loaded at import, the baseline test fixture, and finally a per-test override.",[252,1114,254,1115,242],{},[256,1116,1118],{"id":1117,"viewBox":259,"refX":260,"refY":261,"markerWidth":262,"markerHeight":262,"orient":263},"envsources-a",[265,1119],{"d":267,"fill":414},[270,1121],{"x":272,"y":272,"width":273,"height":274,"rx":275,"fill":276},[278,1123,1109],{"x":280,"y":281,"textAnchor":282,"fontSize":283,"fontWeight":284,"fill":285},[270,1125],{"x":288,"y":289,"width":290,"height":291,"rx":292,"fill":293,"stroke":414,"strokeWidth":294},[278,1127,1128],{"x":297,"y":298,"textAnchor":282,"fontSize":299,"fontWeight":284,"fill":285},"shell \u002F runner",[278,1130,1131],{"x":297,"y":303,"textAnchor":282,"fontSize":304,"fill":285},"uncontrolled",[73,1133],{"x1":308,"y1":309,"x2":310,"y2":309,"stroke":414,"strokeWidth":294,"markerEnd":1134},"url(#envsources-a)",[270,1136],{"x":314,"y":289,"width":290,"height":291,"rx":292,"fill":276,"stroke":414,"strokeWidth":294},[278,1138,1139],{"x":317,"y":298,"textAnchor":282,"fontSize":299,"fontWeight":284,"fill":285},".env loader",[278,1141,1142],{"x":317,"y":303,"textAnchor":282,"fontSize":304,"fill":285},"import time",[73,1144],{"x1":324,"y1":309,"x2":325,"y2":309,"stroke":414,"strokeWidth":294,"markerEnd":1134},[270,1146],{"x":328,"y":289,"width":290,"height":291,"rx":292,"fill":293,"stroke":414,"strokeWidth":294},[278,1148,1149],{"x":331,"y":298,"textAnchor":282,"fontSize":299,"fontWeight":284,"fill":285},"baseline fixture",[278,1151,1152],{"x":331,"y":303,"textAnchor":282,"fontSize":304,"fill":285},"pinned per suite",[73,1154],{"x1":338,"y1":309,"x2":339,"y2":309,"stroke":414,"strokeWidth":294,"markerEnd":1134},[270,1156],{"x":342,"y":289,"width":290,"height":291,"rx":292,"fill":276,"stroke":414,"strokeWidth":294},[278,1158,1159],{"x":345,"y":298,"textAnchor":282,"fontSize":299,"fontWeight":284,"fill":285},"per-test setenv",[278,1161,1162],{"x":345,"y":303,"textAnchor":282,"fontSize":304,"fill":285},"the value under test",[278,1164,1165],{"x":280,"y":352,"textAnchor":282,"fontSize":353,"fontStyle":354,"fill":285},"Disable the dotenv loader in tests, or the second stage silently wins.",[357,1167,1168],{},"Each stage overrides the previous one, so a value that appears wrong is usually being set by a stage nobody remembered.",[19,1170,1172],{"id":1171},"frequently-asked-questions","Frequently Asked Questions",[10,1174,1175,1178,1179,1181],{},[30,1176,1177],{},"Why is my environment variable ignored by the code under test?","\nAlmost always because the value was read at import time into a module-level constant. Setting the variable afterwards changes ",[14,1180,630],{}," but not the constant, so the code keeps using the value captured when the module was first imported.",[10,1183,1184,1187,1188,1190],{},[30,1185,1186],{},"Does monkeypatch.setenv affect subprocesses?","\nYes. It mutates ",[14,1189,630],{}," in the current process, and a subprocess started afterwards inherits that copy. A subprocess started before the change keeps the old environment.",[10,1192,1193,1196,1199,1200,1202,1203,1205,1206,1209,1210,1212],{},[30,1194,1195],{},"How do I remove a variable that may not be set?",[14,1197,1198],{},"monkeypatch.delenv(\"NAME\", raising=False)"," deletes it if present and does nothing otherwise. Without ",[14,1201,130],{}," the call raises ",[14,1204,143],{}," when the variable is absent, which is rarely what a test wants.\n",[30,1207,1208],{},"Can I set environment variables for the whole session?","\nYes, with ",[14,1211,1006],{}," inside a session-scoped fixture, which gives the same undo semantics at a wider scope. Use it for values that are genuinely constant for the run — a test-mode flag, a fixed timezone — and keep per-test values in the function-scoped fixture, or the session value will mask what an individual test tried to set.",[19,1214,1216],{"id":1215},"related","Related",[24,1218,1219,1225,1232,1238],{},[27,1220,1221,1224],{},[42,1222,1223],{"href":44},"Faking the filesystem and environment"," — the parent guide covering paths, cwd and locale.",[27,1226,1227,1231],{},[42,1228,1230],{"href":1229},"\u002Fadvanced-mocking-test-doubles-in-python\u002Ffaking-the-filesystem-and-environment\u002Fusing-tmp-path-instead-of-tempfile-in-tests\u002F","Using tmp_path instead of tempfile in tests"," — the filesystem half of the same isolation problem.",[27,1233,1234,1237],{},[42,1235,1236],{"href":619},"Replacing singletons and module globals in tests"," — why a value read at import cannot be patched later.",[27,1239,1240,1244,1245,1247],{},[42,1241,1243],{"href":1242},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fcontrolling-time-and-randomness-in-tests\u002F","Controlling time and randomness in tests"," — ",[14,1246,902],{}," and seeding, the other environment-driven flakiness.",[10,1249,1250,1251],{},"← Back to ",[42,1252,1253],{"href":44},"Faking the Filesystem and Environment",[1255,1256,1257],"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":67,"searchDepth":80,"depth":80,"links":1259},[1260,1261,1262,1263,1264,1265,1266,1267,1268,1269],{"id":21,"depth":80,"text":22},{"id":49,"depth":80,"text":50},{"id":362,"depth":80,"text":363},{"id":492,"depth":80,"text":493},{"id":624,"depth":80,"text":625},{"id":707,"depth":80,"text":708},{"id":771,"depth":80,"text":772},{"id":982,"depth":80,"text":983},{"id":1171,"depth":80,"text":1172},{"id":1215,"depth":80,"text":1216},"Isolate environment variables in pytest with monkeypatch.setenv and delenv: restoration semantics, config read at import, subprocess inheritance, and leak detection.","md",{"slug":1273,"type":1274,"breadcrumb":55,"datePublished":1275,"dateModified":1275,"faq":1276,"howto":1283},"patching-environment-variables-with-monkeypatch-setenv","article","2026-08-01",[1277,1279,1281],{"q":1177,"a":1278},"Almost always because the value was read at import time into a module-level constant. Setting the variable afterwards changes os.environ but not the constant, so the code keeps using the value captured when the module was first imported.",{"q":1186,"a":1280},"Yes. It mutates os.environ in the current process, and a subprocess started afterwards inherits that copy. A subprocess started before the change keeps the old environment.",{"q":1195,"a":1282},"monkeypatch.delenv('NAME', raising=False) deletes it if present and does nothing otherwise. Without raising=False the call raises KeyError when the variable is absent, which is rarely what a test wants.",{"name":1284,"description":1285,"steps":1286},"How to patch environment variables in pytest","Set, delete and restore environment variables so tests cannot leak state into each other.",[1287,1290,1293,1296],{"name":1288,"text":1289},"Set values with monkeypatch.setenv","Each call records the previous value, or its absence, for restoration at teardown.",{"name":1291,"text":1292},"Delete with delenv and raising False","Remove variables that must be absent without failing when they already are.",{"name":1294,"text":1295},"Re-read configuration after the change","Clear any cache or call the accessor again so the new value is actually observed.",{"name":1297,"text":1298},"Assert no leakage in teardown","Compare os.environ before and after to catch tests that bypassed monkeypatch.","\u002Fadvanced-mocking-test-doubles-in-python\u002Ffaking-the-filesystem-and-environment\u002Fpatching-environment-variables-with-monkeypatch-setenv",{"title":5,"description":1270},"advanced-mocking-test-doubles-in-python\u002Ffaking-the-filesystem-and-environment\u002Fpatching-environment-variables-with-monkeypatch-setenv\u002Findex","l3vPemoipwnsoz3ZRuKoM1VPqNeBy8sKyniTheLw6mY",1785613404451]