[{"data":1,"prerenderedAt":1106},["ShallowReactive",2],{"page-\u002Fadvanced-mocking-test-doubles-in-python\u002Fdependency-injection-for-testability\u002Freplacing-singletons-and-module-globals-in-tests\u002F":3},{"id":4,"title":5,"body":6,"description":1070,"extension":1071,"meta":1072,"navigation":102,"path":1102,"seo":1103,"stem":1104,"__hash__":1105},"content\u002Fadvanced-mocking-test-doubles-in-python\u002Fdependency-injection-for-testability\u002Freplacing-singletons-and-module-globals-in-tests\u002Findex.md","Replacing Singletons and Module Globals in Tests",{"type":7,"value":8,"toc":1059},"minimark",[9,21,26,61,65,68,74,139,150,194,200,265,319,329,494,498,505,607,611,618,621,661,664,682,686,694,773,776,780,783,828,831,834,913,917,986,990,1000,1006,1012,1018,1022,1049,1055],[10,11,12,16,17,20],"p",{},[13,14,15],"code",{},"client = ApiClient(settings.URL)"," at module scope is the most common untestable line in a Python codebase. It runs at import, before any test can intervene; it is copied into every module that does ",[13,18,19],{},"from services import client","; and it holds state that survives the test that dirtied it. Patching it works about half the time, and the other half produces a test that passes while exercising the real object.",[22,23,25],"h2",{"id":24},"prerequisites","Prerequisites",[27,28,29,48],"ul",{},[30,31,32,36,37,40,41,36,44,47],"li",{},[33,34,35],"strong",{},"pytest 7.0+"," for ",[13,38,39],{},"monkeypatch",", and ",[33,42,43],{},"Python 3.9+",[13,45,46],{},"functools.cache",".",[30,49,50,51,56,57,47],{},"The binding rules from ",[52,53,55],"a",{"href":54},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fwhere-to-patch-understanding-mock-patch-targets\u002F","where to patch"," and the seams from ",[52,58,60],{"href":59},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fdependency-injection-for-testability\u002F","dependency injection for testability",[22,62,64],{"id":63},"solution","Solution",[10,66,67],{},"Three refactors, in increasing order of investment. All three can be applied to an existing codebase without touching call sites.",[10,69,70,73],{},[33,71,72],{},"1. Lazy accessor."," Replace the eager module-level object with a cached function. Import cost disappears, and there is now exactly one place where the object comes into existence:",[75,76,81],"pre",{"className":77,"code":78,"language":79,"meta":80,"style":80},"language-python shiki shiki-themes github-light github-dark","# services.py — before\nclient = ApiClient(settings.URL)          # built at import, untestable\n\n# services.py — after\nfrom functools import cache\n\n@cache\ndef get_client() -> ApiClient:\n    return ApiClient(settings.URL)        # built on first use, once\n","python","",[13,82,83,91,97,104,110,116,121,127,133],{"__ignoreMap":80},[84,85,88],"span",{"class":86,"line":87},"line",1,[84,89,90],{},"# services.py — before\n",[84,92,94],{"class":86,"line":93},2,[84,95,96],{},"client = ApiClient(settings.URL)          # built at import, untestable\n",[84,98,100],{"class":86,"line":99},3,[84,101,103],{"emptyLinePlaceholder":102},true,"\n",[84,105,107],{"class":86,"line":106},4,[84,108,109],{},"# services.py — after\n",[84,111,113],{"class":86,"line":112},5,[84,114,115],{},"from functools import cache\n",[84,117,119],{"class":86,"line":118},6,[84,120,103],{"emptyLinePlaceholder":102},[84,122,124],{"class":86,"line":123},7,[84,125,126],{},"@cache\n",[84,128,130],{"class":86,"line":129},8,[84,131,132],{},"def get_client() -> ApiClient:\n",[84,134,136],{"class":86,"line":135},9,[84,137,138],{},"    return ApiClient(settings.URL)        # built on first use, once\n",[10,140,141,142,145,146,149],{},"Call sites change from ",[13,143,144],{},"client.fetch()"," to ",[13,147,148],{},"get_client().fetch()",", which is a mechanical edit and the only change production code sees. Tests can now clear the cache:",[75,151,153],{"className":77,"code":152,"language":79,"meta":80,"style":80},"import pytest\nimport services\n\n@pytest.fixture(autouse=True)\ndef _reset_client_cache():\n    services.get_client.cache_clear()     # start clean\n    yield\n    services.get_client.cache_clear()     # leave clean\n",[13,154,155,160,165,169,174,179,184,189],{"__ignoreMap":80},[84,156,157],{"class":86,"line":87},[84,158,159],{},"import pytest\n",[84,161,162],{"class":86,"line":93},[84,163,164],{},"import services\n",[84,166,167],{"class":86,"line":99},[84,168,103],{"emptyLinePlaceholder":102},[84,170,171],{"class":86,"line":106},[84,172,173],{},"@pytest.fixture(autouse=True)\n",[84,175,176],{"class":86,"line":112},[84,177,178],{},"def _reset_client_cache():\n",[84,180,181],{"class":86,"line":118},[84,182,183],{},"    services.get_client.cache_clear()     # start clean\n",[84,185,186],{"class":86,"line":123},[84,187,188],{},"    yield\n",[84,190,191],{"class":86,"line":129},[84,192,193],{},"    services.get_client.cache_clear()     # leave clean\n",[10,195,196,199],{},[33,197,198],{},"2. Explicit override seam."," A cache clear gives isolation but not substitution. Adding a settable override makes the double injectable without patching:",[75,201,203],{"className":77,"code":202,"language":79,"meta":80,"style":80},"# services.py\n_override: ApiClient | None = None\n\ndef set_client(client: ApiClient | None) -> None:\n    # Test seam: install a double, or pass None to restore normal behaviour.\n    global _override\n    _override = client\n\ndef get_client() -> ApiClient:\n    if _override is not None:\n        return _override\n    return _build_client()                # cached construction as above\n",[13,204,205,210,215,219,224,229,234,239,243,247,253,259],{"__ignoreMap":80},[84,206,207],{"class":86,"line":87},[84,208,209],{},"# services.py\n",[84,211,212],{"class":86,"line":93},[84,213,214],{},"_override: ApiClient | None = None\n",[84,216,217],{"class":86,"line":99},[84,218,103],{"emptyLinePlaceholder":102},[84,220,221],{"class":86,"line":106},[84,222,223],{},"def set_client(client: ApiClient | None) -> None:\n",[84,225,226],{"class":86,"line":112},[84,227,228],{},"    # Test seam: install a double, or pass None to restore normal behaviour.\n",[84,230,231],{"class":86,"line":118},[84,232,233],{},"    global _override\n",[84,235,236],{"class":86,"line":123},[84,237,238],{},"    _override = client\n",[84,240,241],{"class":86,"line":129},[84,242,103],{"emptyLinePlaceholder":102},[84,244,245],{"class":86,"line":135},[84,246,132],{},[84,248,250],{"class":86,"line":249},10,[84,251,252],{},"    if _override is not None:\n",[84,254,256],{"class":86,"line":255},11,[84,257,258],{},"        return _override\n",[84,260,262],{"class":86,"line":261},12,[84,263,264],{},"    return _build_client()                # cached construction as above\n",[75,266,268],{"className":77,"code":267,"language":79,"meta":80,"style":80},"from unittest.mock import create_autospec\nimport pytest, services\nfrom services import ApiClient\n\n@pytest.fixture\ndef fake_client():\n    double = create_autospec(ApiClient, instance=True)\n    services.set_client(double)\n    yield double\n    services.set_client(None)             # always restored, even on failure\n",[13,269,270,275,280,285,289,294,299,304,309,314],{"__ignoreMap":80},[84,271,272],{"class":86,"line":87},[84,273,274],{},"from unittest.mock import create_autospec\n",[84,276,277],{"class":86,"line":93},[84,278,279],{},"import pytest, services\n",[84,281,282],{"class":86,"line":99},[84,283,284],{},"from services import ApiClient\n",[84,286,287],{"class":86,"line":106},[84,288,103],{"emptyLinePlaceholder":102},[84,290,291],{"class":86,"line":112},[84,292,293],{},"@pytest.fixture\n",[84,295,296],{"class":86,"line":118},[84,297,298],{},"def fake_client():\n",[84,300,301],{"class":86,"line":123},[84,302,303],{},"    double = create_autospec(ApiClient, instance=True)\n",[84,305,306],{"class":86,"line":129},[84,307,308],{},"    services.set_client(double)\n",[84,310,311],{"class":86,"line":135},[84,312,313],{},"    yield double\n",[84,315,316],{"class":86,"line":249},[84,317,318],{},"    services.set_client(None)             # always restored, even on failure\n",[10,320,321,324,325,328],{},[33,322,323],{},"3. Full injection."," The end state is that nothing reaches for the global at all: the object is passed to the code that needs it, and only the application entry point calls ",[13,326,327],{},"get_client()",". That is the destination, but the two steps above deliver most of the testability immediately and can ship independently.",[330,331,334,490],"figure",{"className":332},[333],"diagram",[335,336,343,344,343,348,343,352,343,370,343,378,343,387,343,395,343,401,343,406,343,411,343,420,343,425,343,429,343,433,343,437,343,441,343,445,343,449,343,452,343,456,343,460,343,464,343,468,343,472,343,475,343,478,343,482,343,486],"svg",{"viewBox":337,"role":338,"ariaLabelledBy":339,"xmlns":342},"0 0 760 434","img",[340,341],"globalmigrate-t","globalmigrate-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[345,346,347],"title",{"id":340},"Three steps away from a module-level singleton",[349,350,351],"desc",{"id":341},"A vertical progression: an eagerly constructed module global, then a lazily cached accessor, then an accessor with an explicit override seam, and finally full injection where only the entry point resolves the dependency.",[353,354,355,356,343],"defs",{},"\n    ",[357,358,365],"marker",{"id":359,"viewBox":360,"refX":361,"refY":362,"markerWidth":363,"markerHeight":363,"orient":364},"globalmigrate-a","0 0 10 10","9","5","7","auto-start-reverse",[366,367],"path",{"d":368,"fill":369},"M0 0 L10 5 L0 10 z","#81b29a",[371,372],"rect",{"x":373,"y":373,"width":374,"height":375,"rx":376,"fill":377},"0","760","434","14","#fffdf8",[379,380,347],"text",{"x":381,"y":382,"textAnchor":383,"fontSize":384,"fontWeight":385,"fill":386},"380","30","middle","15","700","#3d405b",[371,388],{"x":389,"y":390,"width":391,"height":392,"rx":393,"fill":377,"stroke":369,"strokeWidth":394},"34","58","320","64","12","1.8",[379,396,400],{"x":397,"y":398,"textAnchor":383,"fontSize":399,"fontWeight":385,"fill":386},"194","86","12.5","module-level object",[379,402,405],{"x":397,"y":403,"textAnchor":383,"fontSize":404,"fill":386},"103","11","built at import, untestable",[86,407],{"x1":397,"y1":408,"x2":397,"y2":409,"stroke":369,"strokeWidth":394,"markerEnd":410},"128","149","url(#globalmigrate-a)",[371,412],{"x":413,"y":392,"width":414,"height":415,"rx":416,"fill":377,"stroke":369,"strokeWidth":417,"strokeDashArray":418},"384","346","52","10","1.5",[362,419],"4",[379,421,424],{"x":422,"y":423,"textAnchor":383,"fontSize":404,"fill":386},"557","93","Every importer copies the reference",[371,426],{"x":389,"y":427,"width":391,"height":392,"rx":393,"fill":428,"stroke":369,"strokeWidth":394},"156","#f4f1de",[379,430,432],{"x":397,"y":431,"textAnchor":383,"fontSize":399,"fontWeight":385,"fill":386},"184","cached accessor",[379,434,436],{"x":397,"y":435,"textAnchor":383,"fontSize":404,"fill":386},"201","built on first use, clearable",[86,438],{"x1":397,"y1":439,"x2":397,"y2":440,"stroke":369,"strokeWidth":394,"markerEnd":410},"226","247",[371,442],{"x":413,"y":443,"width":414,"height":415,"rx":416,"fill":428,"stroke":369,"strokeWidth":417,"strokeDashArray":444},"162",[362,419],[379,446,448],{"x":422,"y":447,"textAnchor":383,"fontSize":404,"fill":386},"191","cache_clear() gives isolation",[371,450],{"x":389,"y":451,"width":391,"height":392,"rx":393,"fill":377,"stroke":369,"strokeWidth":394},"254",[379,453,455],{"x":397,"y":454,"textAnchor":383,"fontSize":399,"fontWeight":385,"fill":386},"282","accessor + override",[379,457,459],{"x":397,"y":458,"textAnchor":383,"fontSize":404,"fill":386},"299","substitutable without patching",[86,461],{"x1":397,"y1":462,"x2":397,"y2":463,"stroke":369,"strokeWidth":394,"markerEnd":410},"324","345",[371,465],{"x":413,"y":466,"width":414,"height":415,"rx":416,"fill":377,"stroke":369,"strokeWidth":417,"strokeDashArray":467},"260",[362,419],[379,469,471],{"x":422,"y":470,"textAnchor":383,"fontSize":404,"fill":386},"289","set_x(None) restores normal behaviour",[371,473],{"x":389,"y":474,"width":391,"height":392,"rx":393,"fill":428,"stroke":369,"strokeWidth":394},"352",[379,476,477],{"x":397,"y":381,"textAnchor":383,"fontSize":399,"fontWeight":385,"fill":386},"injected dependency",[379,479,481],{"x":397,"y":480,"textAnchor":383,"fontSize":404,"fill":386},"397","only the entry point resolves it",[371,483],{"x":413,"y":484,"width":414,"height":415,"rx":416,"fill":428,"stroke":369,"strokeWidth":417,"strokeDashArray":485},"358",[362,419],[379,487,489],{"x":422,"y":488,"textAnchor":383,"fontSize":404,"fill":386},"387","The global disappears",[491,492,493],"figcaption",{},"Each step is independently shippable and leaves production behaviour unchanged, which is what makes the migration reviewable.",[22,495,497],{"id":496},"why-this-works","Why this works",[10,499,500,501,504],{},"A module-level assignment binds an object into the module's namespace at import time, and every ",[13,502,503],{},"from module import name"," copies that binding into another namespace. Patching the definition site rebinds one name; the copies keep pointing at the original object, which is why patching global state works only when nothing has imported it by value. A function call, by contrast, is resolved at call time through the module object, so replacing what the function returns affects every caller regardless of how they imported it. That is the entire mechanism behind the accessor pattern: it converts a name lookup performed once at import into a name lookup performed on every use.",[330,506,508,604],{"className":507},[333],[335,509,343,514,343,517,343,520,343,523,343,525,343,531,343,536,343,540,343,544,343,548,343,553,343,557,343,561,343,564,343,567,343,570,343,573,343,576,343,580,343,582,343,585,343,588,343,592,343,594,343,597,343,601],{"viewBox":510,"role":338,"ariaLabelledBy":511,"xmlns":342},"0 0 760 270",[512,513],"globalstrat-t","globalstrat-d",[345,515,516],{"id":512},"Four ways to deal with a global in tests",[349,518,519],{"id":513},"A table comparing patching the module attribute, clearing a cache, an explicit override seam, and full dependency injection, across whether from-imports are handled, whether isolation is automatic, and the production change required.",[371,521],{"x":373,"y":373,"width":374,"height":522,"rx":376,"fill":377},"270",[379,524,516],{"x":381,"y":382,"textAnchor":383,"fontSize":384,"fontWeight":385,"fill":386},[371,526],{"x":527,"y":415,"width":528,"height":529,"rx":416,"fill":530,"stroke":386,"strokeWidth":417},"24","712","40","#f2cc8f",[379,532,535],{"x":533,"y":534,"fontSize":393,"fontWeight":385,"fill":386},"38","76","Criterion",[379,537,539],{"x":538,"y":534,"textAnchor":383,"fontSize":393,"fontWeight":385,"fill":386},"390","Handles from-imports",[379,541,543],{"x":542,"y":534,"textAnchor":383,"fontSize":393,"fontWeight":385,"fill":386},"620","Production change",[371,545],{"x":527,"y":546,"width":528,"height":529,"fill":428,"stroke":386,"strokeWidth":547},"92","1",[379,549,552],{"x":533,"y":550,"fontSize":551,"fill":386},"116","11.5","patch the attribute",[379,554,556],{"x":538,"y":550,"textAnchor":383,"fontSize":551,"fill":555},"#8f3d22","no",[379,558,560],{"x":542,"y":550,"textAnchor":383,"fontSize":551,"fill":559},"#2a5f49","none",[371,562],{"x":527,"y":563,"width":528,"height":529,"fill":377,"stroke":386,"strokeWidth":547},"132",[379,565,566],{"x":533,"y":427,"fontSize":551,"fill":386},"cache_clear() the accessor",[379,568,569],{"x":538,"y":427,"textAnchor":383,"fontSize":551,"fill":559},"yes",[379,571,572],{"x":542,"y":427,"textAnchor":383,"fontSize":551,"fill":555},"call sites",[371,574],{"x":527,"y":575,"width":528,"height":529,"fill":428,"stroke":386,"strokeWidth":547},"172",[379,577,579],{"x":533,"y":578,"fontSize":551,"fill":386},"196","explicit override seam",[379,581,569],{"x":538,"y":578,"textAnchor":383,"fontSize":551,"fill":559},[379,583,584],{"x":542,"y":578,"textAnchor":383,"fontSize":551,"fill":555},"one function",[371,586],{"x":527,"y":587,"width":528,"height":529,"fill":377,"stroke":386,"strokeWidth":547},"212",[379,589,591],{"x":533,"y":590,"fontSize":551,"fill":386},"236","full injection",[379,593,569],{"x":538,"y":590,"textAnchor":383,"fontSize":551,"fill":559},[379,595,596],{"x":542,"y":590,"textAnchor":383,"fontSize":551,"fill":555},"constructor wiring",[86,598],{"x1":599,"y1":415,"x2":599,"y2":600,"stroke":386,"strokeWidth":547},"274","252",[86,602],{"x1":603,"y1":415,"x2":603,"y2":600,"stroke":386,"strokeWidth":547},"505",[491,605,606],{},"Patching is the only approach with no production change, and the only one that fails when another module copied the reference.",[22,608,610],{"id":609},"guarding-against-leaked-state","Guarding against leaked state",[10,612,613,614,617],{},"Whichever approach is used, the risk is the same: a test that installs a double and does not remove it. The failure lands in an unrelated test later in the session, and under ",[13,615,616],{},"pytest-xdist"," it may land on a different worker in a different file.",[10,619,620],{},"A single autouse fixture turns that class of bug into an immediate, attributable failure:",[75,622,624],{"className":77,"code":623,"language":79,"meta":80,"style":80},"import pytest\nimport services\n\n@pytest.fixture(autouse=True)\ndef _no_leaked_client(request):\n    yield\n    if services._override is not None:\n        pytest.fail(f\"{request.node.nodeid} left a client override installed\")\n",[13,625,626,630,634,638,642,647,651,656],{"__ignoreMap":80},[84,627,628],{"class":86,"line":87},[84,629,159],{},[84,631,632],{"class":86,"line":93},[84,633,164],{},[84,635,636],{"class":86,"line":99},[84,637,103],{"emptyLinePlaceholder":102},[84,639,640],{"class":86,"line":106},[84,641,173],{},[84,643,644],{"class":86,"line":112},[84,645,646],{},"def _no_leaked_client(request):\n",[84,648,649],{"class":86,"line":118},[84,650,188],{},[84,652,653],{"class":86,"line":123},[84,654,655],{},"    if services._override is not None:\n",[84,657,658],{"class":86,"line":129},[84,659,660],{},"        pytest.fail(f\"{request.node.nodeid} left a client override installed\")\n",[10,662,663],{},"The check is cheap, runs everywhere, and names the offending test rather than the victim. The same shape works for any global registry: assert at teardown that the registry is the size it was at setup.",[10,665,666,667,670,671,673,674,677,678,47],{},"For globals you do not own — a third-party library's module-level configuration, a logging handler list, an ",[13,668,669],{},"os.environ"," key — prefer ",[13,672,39],{}," over manual assignment, because it restores automatically even when the test raises. The manual form is only justified when the restore logic is more complicated than an assignment, and then it belongs in a fixture with a ",[13,675,676],{},"try\u002Ffinally",", as described in ",[52,679,681],{"href":680},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fpatching-builtins-and-sys-modules-safely\u002F","patching builtins and sys.modules safely",[22,683,685],{"id":684},"testing-the-singleton-itself","Testing the singleton itself",[10,687,688,689,693],{},"One case deserves its own treatment: when the singleton ",[690,691,692],"em",{},"is"," the thing under test — a connection pool, a metrics registry, a feature-flag cache. Here substitution misses the point, and the test needs a real instance with a controlled lifetime.",[75,695,697],{"className":77,"code":696,"language":79,"meta":80,"style":80},"import pytest\nfrom myapp.pool import ConnectionPool\n\n@pytest.fixture\ndef pool():\n    # A fresh instance per test, bypassing the module-level accessor entirely.\n    pool = ConnectionPool(size=2, dsn=\"postgresql:\u002F\u002F\u002Ftest\")\n    yield pool\n    pool.close()\n\ndef test_pool_reuses_connections(pool):\n    a = pool.acquire()\n    pool.release(a)\n    b = pool.acquire()\n    assert a is b                      # the reuse contract, tested on a real pool\n",[13,698,699,703,708,712,716,721,726,731,736,741,745,750,755,761,767],{"__ignoreMap":80},[84,700,701],{"class":86,"line":87},[84,702,159],{},[84,704,705],{"class":86,"line":93},[84,706,707],{},"from myapp.pool import ConnectionPool\n",[84,709,710],{"class":86,"line":99},[84,711,103],{"emptyLinePlaceholder":102},[84,713,714],{"class":86,"line":106},[84,715,293],{},[84,717,718],{"class":86,"line":112},[84,719,720],{},"def pool():\n",[84,722,723],{"class":86,"line":118},[84,724,725],{},"    # A fresh instance per test, bypassing the module-level accessor entirely.\n",[84,727,728],{"class":86,"line":123},[84,729,730],{},"    pool = ConnectionPool(size=2, dsn=\"postgresql:\u002F\u002F\u002Ftest\")\n",[84,732,733],{"class":86,"line":129},[84,734,735],{},"    yield pool\n",[84,737,738],{"class":86,"line":135},[84,739,740],{},"    pool.close()\n",[84,742,743],{"class":86,"line":249},[84,744,103],{"emptyLinePlaceholder":102},[84,746,747],{"class":86,"line":255},[84,748,749],{},"def test_pool_reuses_connections(pool):\n",[84,751,752],{"class":86,"line":261},[84,753,754],{},"    a = pool.acquire()\n",[84,756,758],{"class":86,"line":757},13,[84,759,760],{},"    pool.release(a)\n",[84,762,764],{"class":86,"line":763},14,[84,765,766],{},"    b = pool.acquire()\n",[84,768,770],{"class":86,"line":769},15,[84,771,772],{},"    assert a is b                      # the reuse contract, tested on a real pool\n",[10,774,775],{},"The pattern is the same idea from the other direction: make construction explicit so the test controls the lifetime. A class that can only exist as a module-level singleton is a class that cannot be tested this way, and that constraint — not the global itself — is usually the real design problem.",[22,777,779],{"id":778},"finding-every-global-before-you-start","Finding every global before you start",[10,781,782],{},"The migration is only safe if you know what you are migrating. Three searches find the bindings that matter, and the third is the one people skip.",[75,784,788],{"className":785,"code":786,"language":787,"meta":80,"style":80},"language-console shiki shiki-themes github-light github-dark","# 1. Module-level construction: an assignment at column zero calling something.\n$ grep -rnE '^[a-z_]+ = [A-Z][A-Za-z]*\\(' src\u002F | grep -v 'def \\|class '\n\n# 2. from-imports that copy a binding into another namespace.\n$ grep -rn 'from myapp.services import' src\u002F | grep -v 'import services$'\n\n# 3. Mutable module-level containers — the ones nobody thinks of as singletons.\n$ grep -rnE '^[A-Z_]+ = (\\[\\]|\\{\\}|set\\(\\))' src\u002F\n","console",[13,789,790,795,800,804,809,814,818,823],{"__ignoreMap":80},[84,791,792],{"class":86,"line":87},[84,793,794],{},"# 1. Module-level construction: an assignment at column zero calling something.\n",[84,796,797],{"class":86,"line":93},[84,798,799],{},"$ grep -rnE '^[a-z_]+ = [A-Z][A-Za-z]*\\(' src\u002F | grep -v 'def \\|class '\n",[84,801,802],{"class":86,"line":99},[84,803,103],{"emptyLinePlaceholder":102},[84,805,806],{"class":86,"line":106},[84,807,808],{},"# 2. from-imports that copy a binding into another namespace.\n",[84,810,811],{"class":86,"line":112},[84,812,813],{},"$ grep -rn 'from myapp.services import' src\u002F | grep -v 'import services$'\n",[84,815,816],{"class":86,"line":118},[84,817,103],{"emptyLinePlaceholder":102},[84,819,820],{"class":86,"line":123},[84,821,822],{},"# 3. Mutable module-level containers — the ones nobody thinks of as singletons.\n",[84,824,825],{"class":86,"line":129},[84,826,827],{},"$ grep -rnE '^[A-Z_]+ = (\\[\\]|\\{\\}|set\\(\\))' src\u002F\n",[10,829,830],{},"The third search finds registries, caches and accumulators declared as module constants. They look immutable because the name is uppercase, and they are the most common source of cross-test leakage: a handler list appended to at import time by three modules, a memo dict that grows across the session, a set of registered names that makes the second test see the first one's registration.",[10,832,833],{},"Once the list exists, rank it by how many tests currently patch each entry. A global that five tests already patch is the one to convert first — the conversion deletes five patches and their attendant fragility, which makes the change easy to justify in review.",[330,835,837,910],{"className":836},[333],[335,838,343,842,343,845,343,848,343,850,343,852,343,854,343,856,343,859,343,862,343,864,343,867,343,870,343,873,343,875,343,878,343,881,343,884,343,886,343,889,343,892,343,895,343,897,343,900,343,903,343,906,343,908],{"viewBox":510,"role":338,"ariaLabelledBy":839,"xmlns":342},[840,841],"globalkinds-t","globalkinds-d",[345,843,844],{"id":840},"Three kinds of module-level state",[349,846,847],{"id":841},"A table of three kinds of module-level state - a constructed client, a mutable container, and a configuration value - with how each leaks between tests and the conversion that fixes it.",[371,849],{"x":373,"y":373,"width":374,"height":522,"rx":376,"fill":377},[379,851,844],{"x":381,"y":382,"textAnchor":383,"fontSize":384,"fontWeight":385,"fill":386},[371,853],{"x":527,"y":415,"width":528,"height":529,"rx":416,"fill":530,"stroke":386,"strokeWidth":417},[379,855,535],{"x":533,"y":534,"fontSize":393,"fontWeight":385,"fill":386},[379,857,858],{"x":538,"y":534,"textAnchor":383,"fontSize":393,"fontWeight":385,"fill":386},"Leaks by",[379,860,861],{"x":542,"y":534,"textAnchor":383,"fontSize":393,"fontWeight":385,"fill":386},"Conversion",[371,863],{"x":527,"y":546,"width":528,"height":529,"fill":428,"stroke":386,"strokeWidth":547},[379,865,866],{"x":533,"y":550,"fontSize":551,"fill":386},"constructed object",[379,868,869],{"x":538,"y":550,"textAnchor":383,"fontSize":551,"fill":386},"accumulated state",[379,871,872],{"x":542,"y":550,"textAnchor":383,"fontSize":551,"fill":386},"lazy accessor",[371,874],{"x":527,"y":563,"width":528,"height":529,"fill":377,"stroke":386,"strokeWidth":547},[379,876,877],{"x":533,"y":427,"fontSize":551,"fill":386},"mutable container",[379,879,880],{"x":538,"y":427,"textAnchor":383,"fontSize":551,"fill":386},"appended entries",[379,882,883],{"x":542,"y":427,"textAnchor":383,"fontSize":551,"fill":386},"fixture-owned instance",[371,885],{"x":527,"y":575,"width":528,"height":529,"fill":428,"stroke":386,"strokeWidth":547},[379,887,888],{"x":533,"y":578,"fontSize":551,"fill":386},"config constant",[379,890,891],{"x":538,"y":578,"textAnchor":383,"fontSize":551,"fill":386},"read once at import",[379,893,894],{"x":542,"y":578,"textAnchor":383,"fontSize":551,"fill":386},"read through a function",[371,896],{"x":527,"y":587,"width":528,"height":529,"fill":377,"stroke":386,"strokeWidth":547},[379,898,899],{"x":533,"y":590,"fontSize":551,"fill":386},"registered callbacks",[379,901,902],{"x":538,"y":590,"textAnchor":383,"fontSize":551,"fill":386},"import-time side effects",[379,904,905],{"x":542,"y":590,"textAnchor":383,"fontSize":551,"fill":386},"explicit registration",[86,907],{"x1":599,"y1":415,"x2":599,"y2":600,"stroke":386,"strokeWidth":547},[86,909],{"x1":603,"y1":415,"x2":603,"y2":600,"stroke":386,"strokeWidth":547},[491,911,912],{},"All four are the same bug — state whose lifetime is the process rather than the test — and all four have a one-step conversion.",[22,914,916],{"id":915},"edge-cases-and-failure-modes","Edge cases and failure modes",[27,918,919,931,941,954,962,980],{},[30,920,921,926,927,930],{},[33,922,923,925],{},[13,924,46],{}," on a function with arguments caches per argument tuple",", so an accessor that takes a config key needs ",[13,928,929],{},"cache_clear()"," between tests or the first test's value persists for the second's key set.",[30,932,933,936,937,940],{},[33,934,935],{},"Thread-safety of the override is not free."," A ",[13,938,939],{},"global"," assignment is atomic enough for tests, but a production code path reading it concurrently with a test writing it — in a threaded test suite — is a race; keep overrides out of production execution paths.",[30,942,943,949,950,953],{},[33,944,945,948],{},[13,946,947],{},"del module.attribute"," is not a restore."," Deleting a patched attribute leaves the module without the name, so a later import fails with ",[13,951,952],{},"AttributeError"," rather than reverting to the original.",[30,955,956,961],{},[33,957,958,959,47],{},"Import-time side effects survive ",[13,960,929],{}," If constructing the object also registers a signal handler or starts a thread, clearing the cache creates a second one; make construction idempotent or tear it down explicitly.",[30,963,964,971,972,975,976,47],{},[33,965,966,967,970],{},"Doctest and ",[13,968,969],{},"--import-mode=prepend"," interactions"," can import a module twice under different names, producing two independent globals. ",[13,973,974],{},"importlib"," mode avoids this, as noted in ",[52,977,979],{"href":978},"\u002Fadvanced-pytest-architecture-configuration\u002Fpytest-configuration-best-practices\u002F","pytest configuration best practices",[30,981,982,985],{},[33,983,984],{},"A frozen dataclass singleton is not immutable state."," Its fields may hold mutable containers, so a test that appends to one leaks exactly like any other global.",[22,987,989],{"id":988},"frequently-asked-questions","Frequently Asked Questions",[10,991,992,995,996,999],{},[33,993,994],{},"Why does patching a module global work in one test and not another?","\nBecause the global was already read and bound elsewhere. If another module did ",[13,997,998],{},"from settings import CLIENT"," at import time, that module holds its own reference, and patching the definition site leaves it untouched.",[10,1001,1002,1005],{},[33,1003,1004],{},"Is resetting a singleton between tests good enough?","\nIt works, but it is order-dependent by construction: the reset must run for every test, in the right place, forever. A lazy accessor with an injectable override removes the requirement instead of managing it.",[10,1007,1008,1011],{},[33,1009,1010],{},"How do I test a module-level client created at import time?","\nConvert the eager construction into a cached accessor function. The import then costs nothing, tests can override the cache, and production behaviour is unchanged after the first call.",[10,1013,1014,1017],{},[33,1015,1016],{},"Does monkeypatch undo a singleton reset?","\nIt undoes the attribute assignment it made, not any state the singleton accumulated. If the object mutated internally, restore that state explicitly in the fixture teardown or rebuild the object.",[22,1019,1021],{"id":1020},"related","Related",[27,1023,1024,1030,1036,1042],{},[30,1025,1026,1029],{},[52,1027,1028],{"href":59},"Dependency injection for testability"," — the destination this migration is heading towards.",[30,1031,1032,1035],{},[52,1033,1034],{"href":54},"Where to patch: understanding mock.patch targets"," — why patching a global works only sometimes.",[30,1037,1038,1041],{},[52,1039,1040],{"href":680},"Patching builtins and sys.modules safely"," — restoring process-wide state without leaks.",[30,1043,1044,1048],{},[52,1045,1047],{"href":1046},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fdependency-injection-for-testability\u002Finjecting-fakes-vs-mocks-in-constructors\u002F","Injecting fakes vs mocks in constructors"," — what to pass once the seam exists.",[10,1050,1051,1052],{},"← Back to ",[52,1053,1054],{"href":59},"Dependency Injection for Testability",[1056,1057,1058],"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":80,"searchDepth":93,"depth":93,"links":1060},[1061,1062,1063,1064,1065,1066,1067,1068,1069],{"id":24,"depth":93,"text":25},{"id":63,"depth":93,"text":64},{"id":496,"depth":93,"text":497},{"id":609,"depth":93,"text":610},{"id":684,"depth":93,"text":685},{"id":778,"depth":93,"text":779},{"id":915,"depth":93,"text":916},{"id":988,"depth":93,"text":989},{"id":1020,"depth":93,"text":1021},"Test code that depends on module-level singletons: reset strategies, lazy accessors, monkeypatch scope, and the refactors that remove the global without a rewrite.","md",{"slug":1073,"type":1074,"breadcrumb":1075,"datePublished":1076,"dateModified":1076,"faq":1077,"howto":1086},"replacing-singletons-and-module-globals-in-tests","article","Singletons & Globals","2026-08-01",[1078,1080,1082,1084],{"q":994,"a":1079},"Because the global was already read and bound elsewhere. If another module did from settings import CLIENT at import time, that module holds its own reference, and patching the definition site leaves it untouched.",{"q":1004,"a":1081},"It works, but it is order-dependent by construction: the reset must run for every test, in the right place, forever. A lazy accessor with an injectable override removes the requirement instead of managing it.",{"q":1010,"a":1083},"Convert the eager construction into a cached accessor function. The import then costs nothing, tests can override the cache, and production behaviour is unchanged after the first call.",{"q":1016,"a":1085},"It undoes the attribute assignment it made, not any state the singleton accumulated. If the object mutated internally, restore that state explicitly in the fixture teardown or rebuild the object.",{"name":1087,"description":1088,"steps":1089},"How to replace singletons and module globals in tests","Make global state substitutable without rewriting the application.",[1090,1093,1096,1099],{"name":1091,"text":1092},"Find every binding of the global","Search for both the definition and the from-imports that copied it into other namespaces.",{"name":1094,"text":1095},"Introduce a lazy accessor","Replace the module-level construction with a cached function so nothing is built at import time.",{"name":1097,"text":1098},"Give the accessor an override seam","Allow tests to set and clear the cached value through a single documented function.",{"name":1100,"text":1101},"Assert the reset in teardown","Fail loudly if a test leaves the override in place, so the leak cannot reach another test.","\u002Fadvanced-mocking-test-doubles-in-python\u002Fdependency-injection-for-testability\u002Freplacing-singletons-and-module-globals-in-tests",{"title":5,"description":1070},"advanced-mocking-test-doubles-in-python\u002Fdependency-injection-for-testability\u002Freplacing-singletons-and-module-globals-in-tests\u002Findex","YZ412DZRlfdhJx3w8OXCiqbUqnP7rmFhPL918sDBMXM",1785613404436]