[{"data":1,"prerenderedAt":625},["ShallowReactive",2],{"page-\u002Fadvanced-mocking-test-doubles-in-python\u002Fcontrolling-time-and-randomness-in-tests\u002Ffreezing-time-with-freezegun-vs-monkeypatch\u002F":3},{"id":4,"title":5,"body":6,"description":588,"extension":589,"meta":590,"navigation":116,"path":621,"seo":622,"stem":623,"__hash__":624},"content\u002Fadvanced-mocking-test-doubles-in-python\u002Fcontrolling-time-and-randomness-in-tests\u002Ffreezing-time-with-freezegun-vs-monkeypatch\u002Findex.md","Freezing Time: freezegun vs monkeypatch",{"type":7,"value":8,"toc":581},"minimark",[9,41,46,78,82,327,340,344,384,388,508,512,536,554,572,577],[10,11,12,13,17,18,21,22,25,26,29,30,33,34,36,37,40],"p",{},"You monkeypatch ",[14,15,16],"code",{},"datetime.now"," to a fixed value, the test passes for the function you targeted, and then a helper in another module — or a C-extension serializer, or a ",[14,19,20],{},"time.time()"," call buried in a retry loop — reads the real clock and the test flakes anyway. The question is when a surgical ",[14,23,24],{},"monkeypatch.setattr"," is enough and when you need ",[14,27,28],{},"freezegun"," to replace the clock globally. This guide draws the line: ",[14,31,32],{},"monkeypatch"," controls exactly one name and nothing it cannot see, while ",[14,35,28],{}," swaps the ",[14,38,39],{},"datetime"," class across every module, and neither one stops C code that calls the libc clock directly.",[42,43,45],"h2",{"id":44},"prerequisites","Prerequisites",[47,48,49,60,66],"ul",{},[50,51,52,55,56,59],"li",{},[14,53,54],{},"freezegun >= 1.5"," and ",[14,57,58],{},"pytest >= 8.0",".",[50,61,62,63,59],{},"Python ",[14,64,65],{},"3.9+",[50,67,68,69,72,73,59],{},"For the C-extension gotcha at the end, ",[14,70,71],{},"time-machine >= 2.14"," is the escape hatch, as introduced in ",[74,75,77],"a",{"href":76},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fcontrolling-time-and-randomness-in-tests\u002F","Controlling Time and Randomness in Tests",[42,79,81],{"id":80},"solution","Solution",[83,84,89],"pre",{"className":85,"code":86,"language":87,"meta":88,"style":88},"language-python shiki shiki-themes github-light github-dark","from datetime import datetime, timezone\nimport pytest\nfrom freezegun import freeze_time\n\n# --- code under test, in module myapp.billing ---\n# def invoice_stamp() -> str:\n#     return datetime.now(timezone.utc).isoformat()\n\n# Approach A — monkeypatch a single, known call site.\ndef test_with_monkeypatch(monkeypatch):\n    fixed = datetime(2026, 6, 18, 12, 0, tzinfo=timezone.utc)\n\n    class FrozenDatetime(datetime):\n        @classmethod\n        def now(cls, tz=None):            # only this classmethod is overridden\n            return fixed\n\n    # Patch the NAME as myapp.billing looks it up, not datetime globally.\n    monkeypatch.setattr(\"myapp.billing.datetime\", FrozenDatetime)\n    from myapp.billing import invoice_stamp\n    assert invoice_stamp() == \"2026-06-18T12:00:00+00:00\"\n\n# Approach B — freezegun freezes datetime everywhere at once.\n@freeze_time(\"2026-06-18T12:00:00Z\")\ndef test_with_freezegun():\n    from myapp.billing import invoice_stamp\n    # No per-module patching: every module's datetime.now sees the frozen instant.\n    assert invoice_stamp() == \"2026-06-18T12:00:00+00:00\"\n    # time.time() is frozen too.\n    import time\n    assert time.time() == 1781870400.0\n\n# Advancing a frozen clock with tick().\ndef test_tick_advances_clock():\n    with freeze_time(\"2026-06-18T12:00:00Z\") as frozen:\n        t0 = datetime.now(timezone.utc)\n        frozen.tick()                     # +1 second (default)\n        frozen.tick(delta=59)             # +59 seconds\n        elapsed = (datetime.now(timezone.utc) - t0).total_seconds()\n        assert elapsed == 60\n","python","",[14,90,91,99,105,111,118,124,130,136,141,147,153,159,164,170,176,182,188,193,199,205,211,217,222,228,234,240,245,251,256,262,268,274,279,285,291,297,303,309,315,321],{"__ignoreMap":88},[92,93,96],"span",{"class":94,"line":95},"line",1,[92,97,98],{},"from datetime import datetime, timezone\n",[92,100,102],{"class":94,"line":101},2,[92,103,104],{},"import pytest\n",[92,106,108],{"class":94,"line":107},3,[92,109,110],{},"from freezegun import freeze_time\n",[92,112,114],{"class":94,"line":113},4,[92,115,117],{"emptyLinePlaceholder":116},true,"\n",[92,119,121],{"class":94,"line":120},5,[92,122,123],{},"# --- code under test, in module myapp.billing ---\n",[92,125,127],{"class":94,"line":126},6,[92,128,129],{},"# def invoice_stamp() -> str:\n",[92,131,133],{"class":94,"line":132},7,[92,134,135],{},"#     return datetime.now(timezone.utc).isoformat()\n",[92,137,139],{"class":94,"line":138},8,[92,140,117],{"emptyLinePlaceholder":116},[92,142,144],{"class":94,"line":143},9,[92,145,146],{},"# Approach A — monkeypatch a single, known call site.\n",[92,148,150],{"class":94,"line":149},10,[92,151,152],{},"def test_with_monkeypatch(monkeypatch):\n",[92,154,156],{"class":94,"line":155},11,[92,157,158],{},"    fixed = datetime(2026, 6, 18, 12, 0, tzinfo=timezone.utc)\n",[92,160,162],{"class":94,"line":161},12,[92,163,117],{"emptyLinePlaceholder":116},[92,165,167],{"class":94,"line":166},13,[92,168,169],{},"    class FrozenDatetime(datetime):\n",[92,171,173],{"class":94,"line":172},14,[92,174,175],{},"        @classmethod\n",[92,177,179],{"class":94,"line":178},15,[92,180,181],{},"        def now(cls, tz=None):            # only this classmethod is overridden\n",[92,183,185],{"class":94,"line":184},16,[92,186,187],{},"            return fixed\n",[92,189,191],{"class":94,"line":190},17,[92,192,117],{"emptyLinePlaceholder":116},[92,194,196],{"class":94,"line":195},18,[92,197,198],{},"    # Patch the NAME as myapp.billing looks it up, not datetime globally.\n",[92,200,202],{"class":94,"line":201},19,[92,203,204],{},"    monkeypatch.setattr(\"myapp.billing.datetime\", FrozenDatetime)\n",[92,206,208],{"class":94,"line":207},20,[92,209,210],{},"    from myapp.billing import invoice_stamp\n",[92,212,214],{"class":94,"line":213},21,[92,215,216],{},"    assert invoice_stamp() == \"2026-06-18T12:00:00+00:00\"\n",[92,218,220],{"class":94,"line":219},22,[92,221,117],{"emptyLinePlaceholder":116},[92,223,225],{"class":94,"line":224},23,[92,226,227],{},"# Approach B — freezegun freezes datetime everywhere at once.\n",[92,229,231],{"class":94,"line":230},24,[92,232,233],{},"@freeze_time(\"2026-06-18T12:00:00Z\")\n",[92,235,237],{"class":94,"line":236},25,[92,238,239],{},"def test_with_freezegun():\n",[92,241,243],{"class":94,"line":242},26,[92,244,210],{},[92,246,248],{"class":94,"line":247},27,[92,249,250],{},"    # No per-module patching: every module's datetime.now sees the frozen instant.\n",[92,252,254],{"class":94,"line":253},28,[92,255,216],{},[92,257,259],{"class":94,"line":258},29,[92,260,261],{},"    # time.time() is frozen too.\n",[92,263,265],{"class":94,"line":264},30,[92,266,267],{},"    import time\n",[92,269,271],{"class":94,"line":270},31,[92,272,273],{},"    assert time.time() == 1781870400.0\n",[92,275,277],{"class":94,"line":276},32,[92,278,117],{"emptyLinePlaceholder":116},[92,280,282],{"class":94,"line":281},33,[92,283,284],{},"# Advancing a frozen clock with tick().\n",[92,286,288],{"class":94,"line":287},34,[92,289,290],{},"def test_tick_advances_clock():\n",[92,292,294],{"class":94,"line":293},35,[92,295,296],{},"    with freeze_time(\"2026-06-18T12:00:00Z\") as frozen:\n",[92,298,300],{"class":94,"line":299},36,[92,301,302],{},"        t0 = datetime.now(timezone.utc)\n",[92,304,306],{"class":94,"line":305},37,[92,307,308],{},"        frozen.tick()                     # +1 second (default)\n",[92,310,312],{"class":94,"line":311},38,[92,313,314],{},"        frozen.tick(delta=59)             # +59 seconds\n",[92,316,318],{"class":94,"line":317},39,[92,319,320],{},"        elapsed = (datetime.now(timezone.utc) - t0).total_seconds()\n",[92,322,324],{"class":94,"line":323},40,[92,325,326],{},"        assert elapsed == 60\n",[10,328,329,330,332,333,336,337,59],{},"The decision rule in one line: count the clock reads in the code path. One local name reachable from the test module → ",[14,331,32],{},". More than one, or ",[14,334,335],{},"time.time",", or a transitive call you do not own → ",[14,338,339],{},"freeze_time",[42,341,343],{"id":342},"why-this-works","Why this works",[10,345,346,349,350,352,353,355,356,358,359,362,363,365,366,369,370,372,373,375,376,379,380,383],{},[14,347,348],{},"monkeypatch.setattr(\"myapp.billing.datetime\", ...)"," rebinds a single attribute in a single module's namespace and reverts it on teardown; it is precise and dependency-free, but it is blind to any other module that imported its own ",[14,351,39],{}," reference and to ",[14,354,20],{},". ",[14,357,28],{}," instead walks ",[14,360,361],{},"sys.modules"," and replaces references to the real ",[14,364,39],{}," class with a ",[14,367,368],{},"FakeDatetime"," everywhere, and it patches ",[14,371,335],{},", so transitive calls across modules all observe the same frozen instant. The ",[14,374,339],{}," controller exposes ",[14,377,378],{},"tick()"," because a frozen clock is static by default; ",[14,381,382],{},"tick(delta=...)"," mutates the stored instant so you can assert elapsed-time behaviour without sleeping.",[42,385,387],{"id":386},"edge-cases-and-failure-modes","Edge cases and failure modes",[47,389,390,412,439,463,478],{},[50,391,392,396,397,399,400,402,403,405,406,409,410,59],{},[393,394,395],"strong",{},"C-extension clock reads defeat both."," Code in a compiled extension (or some serializers) that calls the libc clock directly never goes through Python's ",[14,398,39],{}," module, so neither ",[14,401,32],{}," nor ",[14,404,28],{}," touches it. Switch to ",[14,407,408],{},"time-machine",", which patches at the CPython clock level — see ",[74,411,77],{"href":76},[50,413,414,420,421,423,424,426,427,430,431,434,435,59],{},[393,415,416,419],{},[14,417,418],{},"from datetime import datetime"," in the target."," ",[14,422,32],{}," must target the consuming module's ",[14,425,39],{}," name (",[14,428,429],{},"myapp.billing.datetime","), not ",[14,432,433],{},"datetime.datetime",". Targeting the wrong name is the same namespace trap covered in ",[74,436,438],{"href":437},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002F","patching strategies for complex codebases",[50,440,441,446,447,450,451,454,455,458,459,462],{},[393,442,443,445],{},[14,444,28],{}," and naive datetimes."," Freezing to a ",[14,448,449],{},"Z","\u002FUTC string still returns a naive ",[14,452,453],{},"datetime.now()"," unless you call ",[14,456,457],{},"now(timezone.utc)",". Mixing naive and aware datetimes raises ",[14,460,461],{},"TypeError"," on comparison; freeze and read consistently.",[50,464,465,471,472,475,476,59],{},[393,466,467,470],{},[14,468,469],{},"tick=True"," drift."," With ",[14,473,474],{},"freeze_time(..., tick=True)"," the clock advances with real wall time, reintroducing nondeterminism for sub-second assertions. Keep the default static freeze and advance explicitly with ",[14,477,382],{},[50,479,480,490,491,494,495,498,499,502,503,507],{},[393,481,482,485,486,489],{},[14,483,484],{},".start()","\u002F",[14,487,488],{},".stop()"," leakage."," Calling ",[14,492,493],{},"freeze_time(...).start()"," without a matching ",[14,496,497],{},"stop()"," leaks the frozen clock into later tests. Prefer the decorator or ",[14,500,501],{},"with"," form so teardown is guaranteed, as with any ",[74,504,506],{"href":505},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fdeep-dive-into-unittestmock\u002F","unittest.mock"," patch lifecycle.",[42,509,511],{"id":510},"frequently-asked-questions","Frequently Asked Questions",[10,513,514,517,519,520,523,524,526,527,529,530,532,533,535],{},[393,515,516],{},"Why doesn't monkeypatching datetime.now work for some code?",[14,518,433],{}," is a C type, so you cannot set an attribute on it, and code that imported ",[14,521,522],{},"now"," or called ",[14,525,453],{}," in another module still resolves the original. ",[14,528,24],{}," only fixes the one name you target, while ",[14,531,28],{}," replaces the ",[14,534,39],{}," class everywhere it is referenced.",[10,537,538,541,543,544,546,547,550,551,553],{},[393,539,540],{},"How does freezegun's tick() advance a frozen clock?",[14,542,339],{}," returns a controller whose ",[14,545,378],{}," method advances the frozen instant by a timedelta, one second by default. Pass ",[14,548,549],{},"delta"," to advance further. By default the frozen time does not move on its own; ",[14,552,469],{}," makes it advance with real elapsed time.",[10,555,556,559,560,562,563,565,566,568,569,571],{},[393,557,558],{},"Is monkeypatch ever the right choice over freezegun for time?","\nYes, when only one well-known call site reads the clock and you want zero extra dependencies. ",[14,561,24],{}," on that module's ",[14,564,522],{}," reference is faster and explicit, but it does not cover transitive calls, ",[14,567,335],{},", or C-extension clocks the way ",[14,570,28],{}," does.",[10,573,574,575],{},"← Back to ",[74,576,77],{"href":76},[578,579,580],"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":88,"searchDepth":101,"depth":101,"links":582},[583,584,585,586,587],{"id":44,"depth":101,"text":45},{"id":80,"depth":101,"text":81},{"id":342,"depth":101,"text":343},{"id":386,"depth":101,"text":387},{"id":510,"depth":101,"text":511},"Compare freezegun and pytest monkeypatch for freezing time: where each breaks, how tick() works, and the C-extension datetime gotcha that defeats monkeypatch.","md",{"slug":591,"type":592,"breadcrumb":593,"datePublished":594,"dateModified":594,"faq":595,"howto":602},"freezing-time-with-freezegun-vs-monkeypatch","long_tail","freezegun vs monkeypatch","2026-06-18",[596,598,600],{"q":516,"a":597},"datetime.datetime is a C type, so you cannot set an attribute on it, and code that imported now or called datetime.now() in another module still resolves the original. monkeypatch.setattr only fixes the one name you target, while freezegun replaces the datetime class everywhere it is referenced.",{"q":540,"a":599},"freeze_time returns a controller whose tick() method advances the frozen instant by a timedelta, one second by default. Pass delta to advance further. By default the frozen time does not move on its own; tick=True makes it advance with real elapsed time.",{"q":558,"a":601},"Yes, when only one well-known call site reads the clock and you want zero extra dependencies. monkeypatch.setattr on that module's now reference is faster and explicit, but it does not cover transitive calls, time.time, or C-extension clocks the way freezegun does.",{"name":603,"description":604,"steps":605},"How to choose between freezegun and monkeypatch for freezing time","Decide whether to patch a single clock reference with monkeypatch or freeze time globally with freezegun, and advance the clock with tick().",[606,609,612,615,618],{"name":607,"text":608},"Locate every clock read","Find each call to datetime.now, date.today, and time.time in the code path under test to know how many sites need controlling.",{"name":610,"text":611},"Pick the tool","Use monkeypatch when exactly one local name reads the clock; use freeze_time when multiple modules, time.time, or C extensions are involved.",{"name":613,"text":614},"Apply the freeze","Wrap the test with freeze_time or set the single attribute with monkeypatch.setattr at the exact lookup name.",{"name":616,"text":617},"Advance when needed","Use the freeze_time controller's tick(delta=...) to move the clock forward inside the test for time-elapsed assertions.",{"name":619,"text":620},"Verify coverage","Assert that datetime.now inside the block returns the frozen instant before asserting business logic, to confirm the freeze reached the call site.","\u002Fadvanced-mocking-test-doubles-in-python\u002Fcontrolling-time-and-randomness-in-tests\u002Ffreezing-time-with-freezegun-vs-monkeypatch",{"title":5,"description":588},"advanced-mocking-test-doubles-in-python\u002Fcontrolling-time-and-randomness-in-tests\u002Ffreezing-time-with-freezegun-vs-monkeypatch\u002Findex","RBkIowZacHX-qP1YIAOWvgP8GLJNi2VCKnLGxgHcN4g",1781793487754]