[{"data":1,"prerenderedAt":838},["ShallowReactive",2],{"page-\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fpatching-class-attributes-with-patch-object\u002F":3},{"id":4,"title":5,"body":6,"description":802,"extension":803,"meta":804,"navigation":96,"path":834,"seo":835,"stem":836,"__hash__":837},"content\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fpatching-class-attributes-with-patch-object\u002Findex.md","Patching Class Attributes with patch.object",{"type":7,"value":8,"toc":792},"minimark",[9,21,24,39,44,70,74,246,376,380,394,408,411,415,486,490,496,530,536,593,596,600,613,630,643,653,718,722,733,739,748,752,781,788],[10,11,12,16,17,20],"p",{},[13,14,15],"code",{},"patch(\"myapp.billing.Gateway.charge\")"," works until the module is renamed, the class moves, or someone mistypes a segment of the string — at which point it either fails with an import error or, worse, patches something that exists but is not what the code uses. ",[13,18,19],{},"patch.object(Gateway, \"charge\")"," takes the class itself, so the reference is checked by Python at the moment the test module is imported and by the IDE's rename refactoring whenever the class moves.",[10,22,23],{},"It is also the more precise tool. It patches exactly one attribute on exactly one object — a class, an instance, or a module — with no ambiguity about which binding is being replaced. That precision matters most for class-level state: constants, class attributes, and methods whose behaviour should change for every instance at once, or for only one.",[10,25,26,27,30,31,34,35,38],{},"The distinction between the two scopes is the part worth internalising, because choosing wrongly produces tests that either pass for the wrong reason or fail mysteriously. Code that constructs its own collaborators — a service that creates a ",[13,28,29],{},"Gateway()"," internally — can only be affected by patching the class, since the test never holds the instance. Code that receives its collaborators can be tested more precisely by patching the instance the test passes in, leaving every other instance in the process untouched. This guide covers both scopes, the timing rules that decide whether a patch is seen at all, and how ",[13,32,33],{},"patch.object"," relates to pytest's own ",[13,36,37],{},"monkeypatch",".",[40,41,43],"h2",{"id":42},"prerequisites","Prerequisites",[45,46,47,55],"ul",{},[48,49,50,51,54],"li",{},"Python 3.8+; ",[13,52,53],{},"unittest.mock"," in the standard library.",[48,56,57,58,63,64,66,67,38],{},"The target-resolution rules from ",[59,60,62],"a",{"href":61},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fwhere-to-patch-understanding-mock-patch-targets\u002F","where to patch",", which apply to ",[13,65,33],{}," as much as to ",[13,68,69],{},"patch",[40,71,73],{"id":72},"solution","Solution",[75,76,81],"pre",{"className":77,"code":78,"language":79,"meta":80,"style":80},"language-python shiki shiki-themes github-light github-dark","from unittest.mock import patch\n\nfrom myapp.billing import Gateway, RetryPolicy\n\n\ndef test_charge_failure_is_reported():\n    # Class scope: every Gateway instance, including ones made inside the code.\n    with patch.object(Gateway, \"charge\", autospec=True,\n                      side_effect=ConnectionError(\"down\")) as charge:\n        result = checkout(cart_total=4999)\n\n    assert result.status == \"payment_unavailable\"\n    charge.assert_called_once()\n\n\ndef test_single_retry_is_attempted():\n    # A class-level constant, replaced for the duration of the block only.\n    with patch.object(RetryPolicy, \"MAX_RETRIES\", 1):\n        attempts = run_with_policy(RetryPolicy(), always_fails)\n\n    assert attempts == 1\n\n\ndef test_only_this_gateway_is_slow(gateway, other_gateway):\n    # Instance scope: other instances keep the real method.\n    with patch.object(gateway, \"timeout_seconds\", 0.01):\n        assert gateway.timeout_seconds == 0.01\n        assert other_gateway.timeout_seconds == 5.0\n","python","",[13,82,83,91,98,104,109,114,120,126,132,138,144,149,155,161,166,171,177,183,189,195,200,206,211,216,222,228,234,240],{"__ignoreMap":80},[84,85,88],"span",{"class":86,"line":87},"line",1,[84,89,90],{},"from unittest.mock import patch\n",[84,92,94],{"class":86,"line":93},2,[84,95,97],{"emptyLinePlaceholder":96},true,"\n",[84,99,101],{"class":86,"line":100},3,[84,102,103],{},"from myapp.billing import Gateway, RetryPolicy\n",[84,105,107],{"class":86,"line":106},4,[84,108,97],{"emptyLinePlaceholder":96},[84,110,112],{"class":86,"line":111},5,[84,113,97],{"emptyLinePlaceholder":96},[84,115,117],{"class":86,"line":116},6,[84,118,119],{},"def test_charge_failure_is_reported():\n",[84,121,123],{"class":86,"line":122},7,[84,124,125],{},"    # Class scope: every Gateway instance, including ones made inside the code.\n",[84,127,129],{"class":86,"line":128},8,[84,130,131],{},"    with patch.object(Gateway, \"charge\", autospec=True,\n",[84,133,135],{"class":86,"line":134},9,[84,136,137],{},"                      side_effect=ConnectionError(\"down\")) as charge:\n",[84,139,141],{"class":86,"line":140},10,[84,142,143],{},"        result = checkout(cart_total=4999)\n",[84,145,147],{"class":86,"line":146},11,[84,148,97],{"emptyLinePlaceholder":96},[84,150,152],{"class":86,"line":151},12,[84,153,154],{},"    assert result.status == \"payment_unavailable\"\n",[84,156,158],{"class":86,"line":157},13,[84,159,160],{},"    charge.assert_called_once()\n",[84,162,164],{"class":86,"line":163},14,[84,165,97],{"emptyLinePlaceholder":96},[84,167,169],{"class":86,"line":168},15,[84,170,97],{"emptyLinePlaceholder":96},[84,172,174],{"class":86,"line":173},16,[84,175,176],{},"def test_single_retry_is_attempted():\n",[84,178,180],{"class":86,"line":179},17,[84,181,182],{},"    # A class-level constant, replaced for the duration of the block only.\n",[84,184,186],{"class":86,"line":185},18,[84,187,188],{},"    with patch.object(RetryPolicy, \"MAX_RETRIES\", 1):\n",[84,190,192],{"class":86,"line":191},19,[84,193,194],{},"        attempts = run_with_policy(RetryPolicy(), always_fails)\n",[84,196,198],{"class":86,"line":197},20,[84,199,97],{"emptyLinePlaceholder":96},[84,201,203],{"class":86,"line":202},21,[84,204,205],{},"    assert attempts == 1\n",[84,207,209],{"class":86,"line":208},22,[84,210,97],{"emptyLinePlaceholder":96},[84,212,214],{"class":86,"line":213},23,[84,215,97],{"emptyLinePlaceholder":96},[84,217,219],{"class":86,"line":218},24,[84,220,221],{},"def test_only_this_gateway_is_slow(gateway, other_gateway):\n",[84,223,225],{"class":86,"line":224},25,[84,226,227],{},"    # Instance scope: other instances keep the real method.\n",[84,229,231],{"class":86,"line":230},26,[84,232,233],{},"    with patch.object(gateway, \"timeout_seconds\", 0.01):\n",[84,235,237],{"class":86,"line":236},27,[84,238,239],{},"        assert gateway.timeout_seconds == 0.01\n",[84,241,243],{"class":86,"line":242},28,[84,244,245],{},"        assert other_gateway.timeout_seconds == 5.0\n",[247,248,251,372],"figure",{"className":249},[250],"diagram",[252,253,260,261,260,265,260,269,260,277,260,287,260,297,260,302,260,312,260,318,260,321,260,325,260,329,260,333,260,338,260,343,260,347,260,350,260,354,260,358,260,362,260,365,260,368],"svg",{"viewBox":254,"role":255,"ariaLabelledBy":256,"xmlns":259},"0 0 820 262","img",[257,258],"po-t","po-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[262,263,264],"title",{"id":257},"Class-scope versus instance-scope patching",[266,267,268],"desc",{"id":258},"Patching the charge method on the Gateway class replaces it for every instance, including two created inside the code under test. Patching an attribute on one specific gateway instance replaces it only on that instance, leaving a second instance with the original value.",[270,271],"rect",{"x":272,"y":272,"width":273,"height":274,"rx":275,"fill":276},"0","820","262","14","#fffdf8",[278,279,286],"text",{"x":280,"y":281,"textAnchor":282,"fontSize":283,"fontWeight":284,"fill":285},"410","28","middle","16","700","#3d405b","Where the patch lands decides who sees it",[270,288],{"x":289,"y":290,"width":291,"height":292,"rx":293,"fill":294,"stroke":295,"strokeWidth":296},"26","52","368","186","12","#f7f0da","#f2cc8f","2",[278,298,19],{"x":299,"y":300,"textAnchor":282,"fontSize":301,"fontWeight":284,"fill":285},"210","78","12.5",[270,303],{"x":304,"y":305,"width":306,"height":307,"rx":308,"fill":309,"stroke":310,"strokeWidth":311},"50","98","140","44","9","#fbe9e3","#e07a5f","1.8",[278,313,317],{"x":314,"y":315,"textAnchor":282,"fontSize":316,"fill":285},"120","125","11","instance a: patched",[270,319],{"x":299,"y":305,"width":320,"height":307,"rx":308,"fill":309,"stroke":310,"strokeWidth":311},"160",[278,322,324],{"x":323,"y":315,"textAnchor":282,"fontSize":316,"fill":285},"290","instance b: patched",[278,326,328],{"x":299,"y":327,"textAnchor":282,"fontSize":316,"fill":285},"176","methods are looked up on the class,",[278,330,332],{"x":299,"y":331,"textAnchor":282,"fontSize":316,"fill":285},"196","so every instance sees the double —",[278,334,337],{"x":299,"y":335,"textAnchor":282,"fontSize":316,"fill":336},"216","#8a5a00","including ones the code creates",[270,339],{"x":340,"y":290,"width":291,"height":292,"rx":293,"fill":341,"stroke":342,"strokeWidth":296},"426","#e6f0ea","#81b29a",[278,344,346],{"x":345,"y":300,"textAnchor":282,"fontSize":301,"fontWeight":284,"fill":285},"610","patch.object(gateway, \"timeout\")",[270,348],{"x":349,"y":305,"width":306,"height":307,"rx":308,"fill":309,"stroke":310,"strokeWidth":311},"450",[278,351,353],{"x":352,"y":315,"textAnchor":282,"fontSize":316,"fill":285},"520","gateway: patched",[270,355],{"x":345,"y":305,"width":320,"height":307,"rx":308,"fill":276,"stroke":356,"strokeWidth":357},"rgba(61,64,91,0.35)","1.6",[278,359,361],{"x":360,"y":315,"textAnchor":282,"fontSize":316,"fill":285},"690","other: original",[278,363,364],{"x":345,"y":327,"textAnchor":282,"fontSize":316,"fill":285},"instance attribute shadows the",[278,366,367],{"x":345,"y":331,"textAnchor":282,"fontSize":316,"fill":285},"class one for that object only",[278,369,371],{"x":345,"y":335,"textAnchor":282,"fontSize":316,"fill":370},"#2a5f49","precise, and fully restored",[373,374,375],"figcaption",{},"Class scope is right when the code creates its own instances; instance scope is right when the test already holds the object it wants to change.",[40,377,379],{"id":378},"why-this-works","Why this works",[10,381,382,385,386,389,390,393],{},[13,383,384],{},"patch.object(target, name, new)"," does three things: it records ",[13,387,388],{},"getattr(target, name)",", it calls ",[13,391,392],{},"setattr(target, name, new)",", and on exit it restores the recorded value — or deletes the attribute if it did not previously exist on that object. Because Python looks up methods on the class, replacing a method on the class changes behaviour for every instance, including instances created inside the code under test that the test never sees. Replacing an attribute on a single instance adds an instance-level attribute that shadows the class one for that object alone.",[10,395,396,397,400,401,404,405,38],{},"The restore step is what makes the technique safe to use freely. Whether the block exits normally, through a failed assertion, or through an unexpected exception, the original attribute is put back, so one test's patch never leaks into the next. That guarantee holds only for the context-manager and decorator forms, which is why the manual ",[13,398,399],{},"start","\u002F",[13,402,403],{},"stop"," API is best reserved for fixtures that pair them in a ",[13,406,407],{},"finally",[10,409,410],{},"Passing the object rather than a string removes the whole category of patch-target mistakes. There is no module path to resolve, no import that might pick up a different copy, and no string for a refactoring tool to miss.",[40,412,414],{"id":413},"edge-cases-and-failure-modes","Edge cases and failure modes",[45,416,417,436,446,459,469],{},[48,418,419,423,424,427,428,431,432,435],{},[420,421,422],"strong",{},"Constants copied at import."," ",[13,425,426],{},"TIMEOUT = Settings.TIMEOUT"," at module level, or a default argument ",[13,429,430],{},"def f(timeout=Settings.TIMEOUT)",", captures the value once. Patching ",[13,433,434],{},"Settings.TIMEOUT"," later has no effect on those copies. Patch where the value is read.",[48,437,438,441,442,445],{},[420,439,440],{},"Patching a method on an instance with autospec."," The autospecced double for a bound method does not expect ",[13,443,444],{},"self",". On the class it does. Match the scope to the spec.",[48,447,448,423,451,454,455,458],{},[420,449,450],{},"Properties.",[13,452,453],{},"patch.object(Gateway, \"timeout\", 0.01)"," replaces the property descriptor with a plain value on the class, which works but loses the property's logic for every instance. Use ",[13,456,457],{},"new_callable=PropertyMock"," to keep it a property.",[48,460,461,464,465,468],{},[420,462,463],{},"Class methods and static methods."," Patching them on the class needs care, because the descriptor wraps the function. ",[13,466,467],{},"autospec=True"," handles it correctly.",[48,470,471,474,475,478,479,482,483,485],{},[420,472,473],{},"Leaking patches."," The non-context form, ",[13,476,477],{},"patcher.start()",", must be paired with ",[13,480,481],{},"stop()",". Use ",[13,484,37],{}," or the context manager so a failing test still restores.",[40,487,489],{"id":488},"when-a-value-is-read-not-where-it-is-defined","When a value is read, not where it is defined",[10,491,492,493,495],{},"The single most common reason a ",[13,494,33],{}," appears to do nothing is that the code under test does not read the attribute at the moment the test expects. Python binds values at specific times, and a patch applied after the binding changes nothing the code will see.",[10,497,498,499,502,503,506,507,510,511,514,515,518,519,502,522,525,526,529],{},"Three binding times account for nearly every case. ",[420,500,501],{},"Module import",": ",[13,504,505],{},"DEFAULT_TIMEOUT = Settings.TIMEOUT"," at the top of a module captures the value once, when the module is first imported, and every later reference uses that copy. ",[420,508,509],{},"Function definition",": a default argument ",[13,512,513],{},"def fetch(timeout=Settings.TIMEOUT)"," is evaluated once, when the ",[13,516,517],{},"def"," statement runs. ",[420,520,521],{},"Object construction",[13,523,524],{},"self.timeout = Settings.TIMEOUT"," inside ",[13,527,528],{},"__init__"," copies the value into each instance at the moment it is created, so instances built before the patch keep the old value.",[10,531,532,533,535],{},"Only code that reads ",[13,534,434],{}," at call time — inside a function body, on each invocation — sees a patch applied during the test. The practical rule is to find the line where the value is actually read in the code path under test, and patch the object that line reads from. If that line reads a module-level copy, patch the copy; if it reads an instance attribute set at construction, patch the instance or construct it inside the patch.",[247,537,539,590],{"className":538},[250],[252,540,260,545,260,548,260,551,260,555,260,560,260,564,260,570,260,573,260,576,260,579,260,583,260,586],{"viewBox":541,"role":255,"ariaLabelledBy":542,"xmlns":259},"0 0 800 244",[543,544],"bind-t","bind-d",[262,546,547],{"id":543},"When a class attribute's value is captured",[266,549,550],{"id":544},"Four binding times. A module-level copy is captured at import. A default argument is captured at function definition. An instance attribute set in init is captured at construction. Only a read inside a function body at call time sees a patch applied during the test.",[270,552],{"x":272,"y":272,"width":553,"height":554,"rx":275,"fill":276},"800","244",[278,556,559],{"x":557,"y":281,"textAnchor":282,"fontSize":558,"fontWeight":284,"fill":285},"400","15.5","A patch only affects reads that happen after it",[270,561],{"x":289,"y":304,"width":562,"height":563,"rx":308,"fill":309,"stroke":310,"strokeWidth":311},"748","38",[278,565,569],{"x":566,"y":567,"fontSize":568,"fill":285},"46","74","11.5","module import — TIMEOUT = Settings.TIMEOUT → patch too late",[270,571],{"x":289,"y":572,"width":562,"height":563,"rx":308,"fill":309,"stroke":310,"strokeWidth":311},"96",[278,574,575],{"x":566,"y":314,"fontSize":568,"fill":285},"function definition — def f(timeout=Settings.TIMEOUT) → patch too late",[270,577],{"x":289,"y":578,"width":562,"height":563,"rx":308,"fill":294,"stroke":295,"strokeWidth":311},"142",[278,580,582],{"x":566,"y":581,"fontSize":568,"fill":285},"166","construction — self.timeout = Settings.TIMEOUT → only instances built inside the patch",[270,584],{"x":289,"y":585,"width":562,"height":563,"rx":308,"fill":341,"stroke":342,"strokeWidth":311},"188",[278,587,589],{"x":566,"y":588,"fontSize":568,"fill":285},"212","call time — return Settings.TIMEOUT * 2 → sees the patch",[373,591,592],{},"When a patch has no effect, find the line that reads the value and check which of these four rows it belongs to.",[10,594,595],{},"This is also an argument for reading configuration at call time in production code wherever the cost allows. It makes the code patchable without cleverness, and it makes runtime reconfiguration possible for the same reason. Where reading at call time is too expensive, injecting the value through a constructor gives tests the same control without any patching at all.",[40,597,599],{"id":598},"patchobject-versus-monkeypatchsetattr","patch.object versus monkeypatch.setattr",[10,601,602,603,605,606,609,610,612],{},"pytest's ",[13,604,37],{}," fixture offers ",[13,607,608],{},"monkeypatch.setattr(target, name, value)",", which does almost the same thing as ",[13,611,33],{}," and is often the more natural choice inside a pytest suite. The two differ in ways that make each better for particular jobs.",[10,614,615,618,619,622,623,625,626,629],{},[13,616,617],{},"monkeypatch.setattr"," is scoped to the test automatically. Every change is undone at teardown without a context manager or decorator, so a test that patches five attributes reads as five plain lines rather than a nest of ",[13,620,621],{},"with"," blocks. It also accepts a dotted string as its first argument, but the object form is the one to prefer for the same reasons as with ",[13,624,33],{},". What it does not do is create a mock: the value passed is installed as-is, so replacing a method with something that records calls means building the ",[13,627,628],{},"Mock"," explicitly.",[10,631,632,634,635,638,639,642],{},[13,633,33],{}," creates the mock for you, supports ",[13,636,637],{},"autospec"," and ",[13,640,641],{},"spec_set",", and returns the double for assertions. It is the better tool when the replacement is a mock whose calls the test will inspect, and when signature checking matters.",[10,644,645,646,648,649,652],{},"A common and effective combination uses both: ",[13,647,617],{}," for plain values — constants, feature flags, configuration — where no call recording is needed, and ",[13,650,651],{},"patch.object(..., autospec=True)"," for methods whose calls the test asserts on. The result reads cleanly — configuration changes as flat lines at the top of the test, method doubles as explicit context managers around the action — and each tool does the part of the job it is designed for, with neither stretched to cover the other's case.",[247,654,656,715],{"className":655},[250],[252,657,260,662,260,665,260,668,260,671,260,674,260,678,260,682,260,686,260,690,260,693,260,696,260,699,260,702,260,706,260,709,260,712],{"viewBox":658,"role":255,"ariaLabelledBy":659,"xmlns":259},"0 0 800 236",[660,661],"mp-t","mp-d",[262,663,664],{"id":660},"patch.object and monkeypatch.setattr compared",[266,666,667],{"id":661},"Two tools. monkeypatch.setattr is undone automatically at teardown and installs a plain value, suiting constants and flags. patch.object creates a mock, supports autospec and spec_set, and returns the double for assertions, suiting methods whose calls the test inspects.",[270,669],{"x":272,"y":272,"width":553,"height":670,"rx":275,"fill":276},"236",[278,672,673],{"x":557,"y":281,"textAnchor":282,"fontSize":558,"fontWeight":284,"fill":285},"Values with one, recorded calls with the other",[270,675],{"x":289,"y":304,"width":676,"height":677,"rx":293,"fill":341,"stroke":342,"strokeWidth":296},"360","164",[278,679,617],{"x":680,"y":681,"textAnchor":282,"fontSize":301,"fontWeight":284,"fill":285},"206","76",[278,683,685],{"x":307,"y":684,"fontSize":316,"fill":285},"104","undone at teardown automatically",[278,687,689],{"x":307,"y":688,"fontSize":316,"fill":285},"126","installs the value you pass",[278,691,692],{"x":307,"y":677,"fontSize":316,"fontWeight":284,"fill":370},"constants, flags, config",[278,694,695],{"x":307,"y":292,"fontSize":316,"fill":285},"no nesting, one line each",[270,697],{"x":698,"y":304,"width":676,"height":677,"rx":293,"fill":294,"stroke":295,"strokeWidth":296},"414",[278,700,33],{"x":701,"y":681,"textAnchor":282,"fontSize":301,"fontWeight":284,"fill":285},"594",[278,703,705],{"x":704,"y":684,"fontSize":316,"fill":285},"432","creates the mock for you",[278,707,708],{"x":704,"y":688,"fontSize":316,"fill":285},"autospec, spec_set, return value",[278,710,711],{"x":704,"y":677,"fontSize":316,"fontWeight":284,"fill":336},"methods you assert on",[278,713,714],{"x":704,"y":292,"fontSize":316,"fill":285},"signature checking included",[373,716,717],{},"Using each for what it does best keeps tests short without giving up the signature checks that make method doubles trustworthy.",[40,719,721],{"id":720},"frequently-asked-questions","Frequently Asked Questions",[10,723,724,727,729,730,732],{},[420,725,726],{},"What is the difference between patch and patch.object?",[13,728,69],{}," takes a dotted string and resolves it by importing the module at patch time. ",[13,731,33],{}," takes the object itself and the attribute name, so there is no string to get wrong and no import-time lookup. Both restore the original when the context exits.",[10,734,735,738],{},[420,736,737],{},"Does patching a method on the class affect existing instances?","\nYes. Methods are looked up on the class, so patching the class attribute changes behaviour for every instance, including ones created before the patch. Patching an instance attribute affects only that instance.",[10,740,741,744,747],{},[420,742,743],{},"How do I patch a class-level constant?",[13,745,746],{},"patch.object(MyClass, \"MAX_RETRIES\", 1)"," replaces the value for the duration of the context. Code that copied the constant into a local variable or a default argument at import time will not see the change; patch where the value is read.",[40,749,751],{"id":750},"related","Related",[45,753,754,760,767,774],{},[48,755,756,759],{},[59,757,758],{"href":61},"Where to Patch: Understanding mock.patch Targets"," — the lookup rules behind every patch.",[48,761,762,766],{},[59,763,765],{"href":764},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fstacking-multiple-patches-without-argument-confusion\u002F","Stacking Multiple Patches Without Argument Confusion"," — combining several patch.object calls cleanly.",[48,768,769,773],{},[59,770,772],{"href":771},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fautospec-strict-mocking\u002Fmocking-properties-and-class-attributes-with-autospec\u002F","Mocking Properties and Class Attributes with Autospec"," — the property case in detail.",[48,775,776,780],{},[59,777,779],{"href":778},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fspies-fakes-and-hand-rolled-test-doubles\u002Fspying-on-a-real-object-with-wraps\u002F","Spying on a Real Object with wraps"," — patch.object with wraps to observe rather than replace.",[10,782,783,784],{},"← Back to ",[59,785,787],{"href":786},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002F","Patching Strategies for Complex Codebases",[789,790,791],"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":793},[794,795,796,797,798,799,800,801],{"id":42,"depth":93,"text":43},{"id":72,"depth":93,"text":73},{"id":378,"depth":93,"text":379},{"id":413,"depth":93,"text":414},{"id":488,"depth":93,"text":489},{"id":598,"depth":93,"text":599},{"id":720,"depth":93,"text":721},{"id":750,"depth":93,"text":751},"Use patch.object to replace a method, class attribute or constant on a specific object: class versus instance scope, restoration, autospec, and when it beats string targets.","md",{"slug":805,"type":806,"breadcrumb":33,"datePublished":807,"dateModified":807,"faq":808,"howto":815},"patching-class-attributes-with-patch-object","article","2026-09-18",[809,811,813],{"q":726,"a":810},"patch takes a dotted string and resolves it by importing the module at patch time. patch.object takes the object itself and the attribute name, so there is no string to get wrong and no import-time lookup. Both restore the original when the context exits.",{"q":737,"a":812},"Yes. Methods are looked up on the class, so patching the class attribute changes behaviour for every instance, including ones created before the patch. Patching an instance attribute affects only that instance.",{"q":743,"a":814},"patch.object(MyClass, 'MAX_RETRIES', 1) replaces the value for the duration of the context. Code that copied the constant into a local variable or a default argument at import time will not see the change; patch where the value is read.",{"name":816,"description":817,"steps":818},"How to patch a class attribute with patch.object","Choose class or instance scope, patch the object where the code looks the attribute up, and let the context manager restore it.",[819,822,825,828,831],{"name":820,"text":821},"Identify where the attribute is looked up","Decide whether the code reads the attribute from the class, from an instance, or from a copy made at import.",{"name":823,"text":824},"Choose class or instance scope","Patch the class to affect every instance; patch one instance to affect only it.",{"name":826,"text":827},"Use the object, not a string","Pass the class or instance directly to patch.object so there is nothing to misspell.",{"name":829,"text":830},"Add autospec for methods","Use autospec=True when replacing a method so calls are checked against its real signature.",{"name":832,"text":833},"Let the context restore it","Use the context manager or decorator form so the original is restored even if the test fails.","\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fpatching-class-attributes-with-patch-object",{"title":5,"description":802},"advanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fpatching-class-attributes-with-patch-object\u002Findex","MDbNlTBD57TD6W2PI_OnD4DUCIUZRy0had2vUQIJZos",1789718767607]