[{"data":1,"prerenderedAt":954},["ShallowReactive",2],{"page-\u002Fadvanced-mocking-test-doubles-in-python\u002Fautospec-strict-mocking\u002Fcatching-signature-drift-with-spec-set\u002F":3},{"id":4,"title":5,"body":6,"description":918,"extension":919,"meta":920,"navigation":127,"path":950,"seo":951,"stem":952,"__hash__":953},"content\u002Fadvanced-mocking-test-doubles-in-python\u002Fautospec-strict-mocking\u002Fcatching-signature-drift-with-spec-set\u002Findex.md","Catching Signature Drift with spec_set",{"type":7,"value":8,"toc":907},"minimark",[9,39,49,71,76,95,99,257,401,405,415,421,431,435,501,505,528,534,544,622,626,649,652,690,707,792,796,806,809,813,837,850,856,860,898,903],[10,11,12,13,17,18,21,22,26,27,30,31,34,35,38],"p",{},"Autospec checks how a mock is ",[14,15,16],"em",{},"called",". It does not check how a test ",[14,19,20],{},"configures"," it. A test that sets ",[23,24,25],"code",{},"client.retrys = 3"," on an autospecced client — misspelling ",[23,28,29],{},"retries"," — succeeds silently: the mock gains a new attribute, the code under test reads ",[23,32,33],{},"client.retries",", gets an auto-created child mock instead of ",[23,36,37],{},"3",", and the test either passes for the wrong reason or fails somewhere far from the typo. The same thing happens, more insidiously, when the real class renames an attribute and the test keeps setting the old name.",[10,40,41,44,45,48],{},[23,42,43],{},"spec_set"," closes that gap. A mock created with it rejects assignment to any attribute the real object does not have, so the misspelling or the stale name raises ",[23,46,47],{},"AttributeError"," on the line that set it. It is a one-word change with an outsized effect on how quickly configuration drift is noticed.",[10,50,51,52,55,56,59,60,63,64,67,68,70],{},"The underlying issue is that ",[23,53,54],{},"unittest.mock"," is permissive by design. A plain ",[23,57,58],{},"Mock"," or ",[23,61,62],{},"MagicMock"," accepts any attribute read, any attribute write and any call, because it was built for exploratory use where anything goes. Each of ",[23,65,66],{},"spec",", autospec and ",[23,69,43],{}," removes one of those freedoms. Most suites adopt the first two and stop, leaving writes unchecked — and writes are precisely how tests configure the state the code under test reads. Closing that last gap costs one keyword argument per mock factory and changes no test that was already correct.",[72,73,75],"h2",{"id":74},"prerequisites","Prerequisites",[77,78,79,86],"ul",{},[80,81,82,83,85],"li",{},"Python 3.8+; ",[23,84,54],{}," in the standard library.",[80,87,88,89,94],{},"The autospec fundamentals from ",[90,91,93],"a",{"href":92},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fautospec-strict-mocking\u002F","autospec and strict mocking",".",[72,96,98],{"id":97},"solution","Solution",[100,101,106],"pre",{"className":102,"code":103,"language":104,"meta":105,"style":105},"language-python shiki shiki-themes github-light github-dark","from dataclasses import dataclass\nfrom unittest.mock import create_autospec\n\nimport pytest\n\n\nclass HttpClient:\n    retries: int = 3                          # class-level: visible to the spec\n    timeout: float = 5.0\n\n    def get(self, path: str) -> dict: ...\n\n\ndef test_misspelt_configuration_fails_immediately():\n    client = create_autospec(HttpClient, instance=True, spec_set=True)\n\n    with pytest.raises(AttributeError, match=\"retrys\"):\n        client.retrys = 0                     # the typo is caught at assignment\n\n\ndef test_real_configuration_is_accepted():\n    client = create_autospec(HttpClient, instance=True, spec_set=True)\n    client.retries = 0                        # a real attribute: fine\n    client.get.return_value = {\"ok\": True}\n\n    assert fetch_without_retry(client) == {\"ok\": True}\n","python","",[23,107,108,116,122,129,135,140,145,151,157,163,168,174,179,184,190,196,201,207,213,218,223,229,234,240,246,251],{"__ignoreMap":105},[109,110,113],"span",{"class":111,"line":112},"line",1,[109,114,115],{},"from dataclasses import dataclass\n",[109,117,119],{"class":111,"line":118},2,[109,120,121],{},"from unittest.mock import create_autospec\n",[109,123,125],{"class":111,"line":124},3,[109,126,128],{"emptyLinePlaceholder":127},true,"\n",[109,130,132],{"class":111,"line":131},4,[109,133,134],{},"import pytest\n",[109,136,138],{"class":111,"line":137},5,[109,139,128],{"emptyLinePlaceholder":127},[109,141,143],{"class":111,"line":142},6,[109,144,128],{"emptyLinePlaceholder":127},[109,146,148],{"class":111,"line":147},7,[109,149,150],{},"class HttpClient:\n",[109,152,154],{"class":111,"line":153},8,[109,155,156],{},"    retries: int = 3                          # class-level: visible to the spec\n",[109,158,160],{"class":111,"line":159},9,[109,161,162],{},"    timeout: float = 5.0\n",[109,164,166],{"class":111,"line":165},10,[109,167,128],{"emptyLinePlaceholder":127},[109,169,171],{"class":111,"line":170},11,[109,172,173],{},"    def get(self, path: str) -> dict: ...\n",[109,175,177],{"class":111,"line":176},12,[109,178,128],{"emptyLinePlaceholder":127},[109,180,182],{"class":111,"line":181},13,[109,183,128],{"emptyLinePlaceholder":127},[109,185,187],{"class":111,"line":186},14,[109,188,189],{},"def test_misspelt_configuration_fails_immediately():\n",[109,191,193],{"class":111,"line":192},15,[109,194,195],{},"    client = create_autospec(HttpClient, instance=True, spec_set=True)\n",[109,197,199],{"class":111,"line":198},16,[109,200,128],{"emptyLinePlaceholder":127},[109,202,204],{"class":111,"line":203},17,[109,205,206],{},"    with pytest.raises(AttributeError, match=\"retrys\"):\n",[109,208,210],{"class":111,"line":209},18,[109,211,212],{},"        client.retrys = 0                     # the typo is caught at assignment\n",[109,214,216],{"class":111,"line":215},19,[109,217,128],{"emptyLinePlaceholder":127},[109,219,221],{"class":111,"line":220},20,[109,222,128],{"emptyLinePlaceholder":127},[109,224,226],{"class":111,"line":225},21,[109,227,228],{},"def test_real_configuration_is_accepted():\n",[109,230,232],{"class":111,"line":231},22,[109,233,195],{},[109,235,237],{"class":111,"line":236},23,[109,238,239],{},"    client.retries = 0                        # a real attribute: fine\n",[109,241,243],{"class":111,"line":242},24,[109,244,245],{},"    client.get.return_value = {\"ok\": True}\n",[109,247,249],{"class":111,"line":248},25,[109,250,128],{"emptyLinePlaceholder":127},[109,252,254],{"class":111,"line":253},26,[109,255,256],{},"    assert fetch_without_retry(client) == {\"ok\": True}\n",[258,259,262,394],"figure",{"className":260},[261],"diagram",[263,264,271,272,271,276,271,280,271,288,271,298,271,307,271,313,271,320,271,323,271,326,271,330,271,337,271,341,271,346,271,350,271,352,271,354,271,358,271,362,271,364,271,366,271,370,271,372,271,374,271,376,271,382,271,385,271,390],"svg",{"viewBox":265,"role":266,"ariaLabelledBy":267,"xmlns":270},"0 0 820 262","img",[268,269],"ss-t","ss-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[273,274,275],"title",{"id":268},"What spec, autospec and spec_set each check",[277,278,279],"desc",{"id":269},"Three levels of strictness. spec rejects reading attributes the real object lacks. autospec additionally enforces call signatures. spec_set additionally rejects assigning attributes the real object lacks. Only spec_set catches a test that configures a misspelt or renamed attribute.",[281,282],"rect",{"x":283,"y":283,"width":284,"height":285,"rx":286,"fill":287},"0","820","262","14","#fffdf8",[289,290,297],"text",{"x":291,"y":292,"textAnchor":293,"fontSize":294,"fontWeight":295,"fill":296},"410","28","middle","16","700","#3d405b","Reads, calls, and writes — three different checks",[281,299],{"x":300,"y":301,"width":302,"height":303,"rx":304,"fill":305,"stroke":296,"strokeWidth":306},"26","48","220","36","9","#f4f1de","1.5",[289,308,312],{"x":309,"y":310,"textAnchor":293,"fontSize":311,"fontWeight":295,"fill":296},"136","71","11.5","check",[281,314],{"x":315,"y":301,"width":316,"height":303,"rx":304,"fill":317,"stroke":318,"strokeWidth":319},"256","170","#f7f0da","#f2cc8f","1.8",[289,321,66],{"x":322,"y":310,"textAnchor":293,"fontSize":311,"fontWeight":295,"fill":296},"341",[281,324],{"x":325,"y":301,"width":316,"height":303,"rx":304,"fill":317,"stroke":318,"strokeWidth":319},"436",[289,327,329],{"x":328,"y":310,"textAnchor":293,"fontSize":311,"fontWeight":295,"fill":296},"521","autospec",[281,331],{"x":332,"y":301,"width":333,"height":303,"rx":304,"fill":334,"stroke":335,"strokeWidth":336},"616","178","#e6f0ea","#81b29a","2",[289,338,340],{"x":339,"y":310,"textAnchor":293,"fontSize":311,"fontWeight":295,"fill":296},"705","+ spec_set",[289,342,345],{"x":343,"y":344,"fontSize":311,"fill":296},"42","114","mock.unknown (read)",[289,347,349],{"x":322,"y":344,"textAnchor":293,"fontSize":311,"fill":348},"#2a5f49","rejected",[289,351,349],{"x":328,"y":344,"textAnchor":293,"fontSize":311,"fill":348},[289,353,349],{"x":339,"y":344,"textAnchor":293,"fontSize":311,"fill":348},[289,355,357],{"x":343,"y":356,"fontSize":311,"fill":296},"152","mock.get(wrong_arg=1)",[289,359,361],{"x":322,"y":356,"textAnchor":293,"fontSize":311,"fill":360},"#8f3d22","accepted",[289,363,349],{"x":328,"y":356,"textAnchor":293,"fontSize":311,"fill":348},[289,365,349],{"x":339,"y":356,"textAnchor":293,"fontSize":311,"fill":348},[289,367,369],{"x":343,"y":368,"fontSize":311,"fill":296},"190","mock.retrys = 0 (write)",[289,371,361],{"x":322,"y":368,"textAnchor":293,"fontSize":311,"fill":360},[289,373,361],{"x":328,"y":368,"textAnchor":293,"fontSize":311,"fill":360},[289,375,349],{"x":339,"y":368,"textAnchor":293,"fontSize":311,"fill":348},[111,377],{"x1":300,"y1":378,"x2":379,"y2":378,"stroke":380,"strokeWidth":381},"128","794","rgba(61,64,91,0.14)","1.4",[111,383],{"x1":300,"y1":384,"x2":379,"y2":384,"stroke":380,"strokeWidth":381},"166",[281,386],{"x":300,"y":387,"width":388,"height":303,"rx":304,"fill":287,"stroke":389,"strokeWidth":381},"210","768","rgba(61,64,91,0.35)",[289,391,393],{"x":291,"y":392,"textAnchor":293,"fontSize":311,"fill":296},"233","The bottom row is where configuration typos and stale attribute names hide.",[395,396,397,398,400],"figcaption",{},"Autospec plus ",[23,399,43],{}," is the only combination that checks every way a test interacts with a mock.",[72,402,404],{"id":403},"why-this-works","Why this works",[10,406,407,408,410,411,414],{},"A mock with a spec keeps a list of the attribute names the specification object exposes. Reading an attribute not on the list raises. ",[23,409,43],{}," makes the mock consult the same list on assignment, through its ",[23,412,413],{},"__setattr__",", and raise for any name not present. Because the list comes from the real class, any attribute the real class renames or removes becomes unassignable on the mock the moment the class changes — and every test that still configures the old name fails, on the configuring line, with the old name in the message.",[10,416,417,418,420],{},"The check happens at assignment time, which is the earliest possible moment: the failing line is the one with the wrong name on it, not some later line where the code reads a value it did not expect. That locality is what makes ",[23,419,43],{}," failures quick to fix compared with the confusing downstream failures a silently-accepted typo produces.",[10,422,423,426,427,430],{},[23,424,425],{},"create_autospec(..., spec_set=True)"," combines this with signature enforcement, and applies it recursively to child attributes, so a nested configuration such as ",[23,428,429],{},"client.session.timeout = 1"," is checked at every level.",[72,432,434],{"id":433},"edge-cases-and-failure-modes","Edge cases and failure modes",[77,436,437,457,466,479,488],{},[80,438,439,446,447,450,451,453,454,456],{},[440,441,442,443,94],"strong",{},"Attributes set only in ",[23,444,445],{},"__init__"," ",[23,448,449],{},"self.retries = 3"," inside ",[23,452,445],{}," is invisible to a class-based spec, so ",[23,455,43],{}," rejects a legitimate assignment. Declare the attribute as a class-level annotation, or spec from an instance.",[80,458,459,462,463,465],{},[440,460,461],{},"Dataclasses."," Fields are class-level annotations, so ",[23,464,43],{}," sees them — dataclasses work well with it out of the box.",[80,467,468,471,472,474,475,478],{},[440,469,470],{},"Properties."," Assigning to a mocked property with ",[23,473,43],{}," fails because properties are read-only on the class. Configure them with ",[23,476,477],{},"PropertyMock"," on the type instead.",[80,480,481,487],{},[440,482,483,486],{},[23,484,485],{},"__slots__"," classes."," The slot names are visible to the spec and work normally.",[80,489,490,493,494,497,498,500],{},[440,491,492],{},"Dynamic attributes."," Objects that accept arbitrary attributes by design — configuration bags, ",[23,495,496],{},"SimpleNamespace"," — defeat the point of ",[23,499,43],{},". Declare a dataclass for the fields the code reads and spec against that.",[72,502,504],{"id":503},"where-drift-actually-comes-from","Where drift actually comes from",[10,506,507,508,510,511,514,515,518,519,521,522,524,525,527],{},"The failure ",[23,509,43],{}," prevents is rarely a typo on the day a test is written; the author usually runs the test and notices. It is a rename months later. A class's ",[23,512,513],{},"timeout_seconds"," becomes ",[23,516,517],{},"timeout",", the production code is updated, the type checker is satisfied, and forty tests continue to set ",[23,520,513],{}," on their mocks. Without ",[23,523,43],{},", those tests still pass — the attribute they set is simply never read, and the code reads an auto-created child mock for ",[23,526,517],{}," instead. The tests have stopped testing what they claim to test, and nothing indicates it.",[10,529,530,531,533],{},"With ",[23,532,43],{},", the rename breaks all forty on the line that sets the old name. That feels like a cost at the moment of the rename and is actually the whole point: the forty tests were about to become silently meaningless, and the failures are a precise list of the places that need updating. A search-and-replace fixes them in a minute, and every one of them is then testing the real attribute again.",[10,535,536,537,540,541,543],{},"Configuration and settings objects are where this pays off most, because they are read in many places and renamed during refactors more often than behavioural classes. A ",[23,538,539],{},"Settings"," dataclass mocked with ",[23,542,43],{}," across a suite turns every settings rename into a compile-time-like check across every test that configures it. Often the better move for settings is not to mock them at all but to construct a real instance with overridden values — a dataclass with defaults is its own best test double — which gets the same protection from the constructor's own argument checking. A misspelt keyword to a dataclass constructor fails immediately, with no mocking library involved.",[258,545,547,619],{"className":546},[261],[263,548,271,553,271,556,271,559,271,563,271,568,271,576,271,582,271,588,271,592,271,595,271,599,271,602,271,606,271,610,271,613,271,616],{"viewBox":549,"role":266,"ariaLabelledBy":550,"xmlns":270},"0 0 800 236",[551,552],"dr-t","dr-d",[273,554,555],{"id":551},"A rename with and without spec_set",[277,557,558],{"id":552},"A class renames timeout_seconds to timeout. Without spec_set, forty tests keep setting the old name, the code reads an auto-created mock for the new name, and the tests pass while testing nothing. With spec_set, all forty fail on the line that sets the old name, giving a precise list of places to update.",[281,560],{"x":283,"y":283,"width":561,"height":562,"rx":286,"fill":287},"800","236",[289,564,567],{"x":565,"y":292,"textAnchor":293,"fontSize":566,"fontWeight":295,"fill":296},"400","15.5","timeout_seconds → timeout",[281,569],{"x":300,"y":570,"width":571,"height":572,"rx":573,"fill":574,"stroke":575,"strokeWidth":336},"50","360","164","12","#fbe9e3","#e07a5f",[289,577,581],{"x":578,"y":579,"textAnchor":293,"fontSize":580,"fontWeight":295,"fill":296},"206","76","12.5","without spec_set",[289,583,587],{"x":584,"y":585,"fontSize":586,"fill":296},"44","104","11","tests set mock.timeout_seconds = 1",[289,589,591],{"x":584,"y":590,"fontSize":586,"fill":296},"126","code reads mock.timeout → a Mock",[289,593,594],{"x":584,"y":572,"fontSize":586,"fontWeight":295,"fill":360},"40 tests green, testing nothing",[289,596,598],{"x":584,"y":597,"fontSize":586,"fill":296},"186","no signal anything changed",[281,600],{"x":601,"y":570,"width":571,"height":572,"rx":573,"fill":334,"stroke":335,"strokeWidth":336},"414",[289,603,605],{"x":604,"y":579,"textAnchor":293,"fontSize":580,"fontWeight":295,"fill":296},"594","with spec_set",[289,607,609],{"x":608,"y":585,"fontSize":586,"fill":296},"432","mock.timeout_seconds = 1",[289,611,612],{"x":608,"y":590,"fontSize":586,"fill":296},"→ AttributeError on that line",[289,614,615],{"x":608,"y":572,"fontSize":586,"fontWeight":295,"fill":348},"40 precise failures",[289,617,618],{"x":608,"y":597,"fontSize":586,"fill":296},"fixed by one search-and-replace",[395,620,621],{},"Forty red tests is a better outcome than forty green tests that stopped meaning anything — and it takes a minute to fix.",[72,623,625],{"id":624},"making-instance-attributes-visible-to-the-spec","Making instance attributes visible to the spec",[10,627,628,629,631,632,634,635,638,639,641,642,645,646,648],{},"The most common reason teams abandon ",[23,630,43],{}," is the first legitimate assignment it rejects. A class that sets its attributes in ",[23,633,445],{}," — ",[23,636,637],{},"self.retries = retries"," — exposes nothing at class level, so a class-based spec does not know ",[23,640,29],{}," exists, and ",[23,643,644],{},"client.retries = 0"," on the mock raises. It looks like ",[23,647,43],{}," being unreasonable; it is actually the spec being incomplete.",[10,650,651],{},"There are three fixes, in order of preference. The first is to declare the attributes as class-level annotations, which is good practice anyway for type checkers and costs one line per attribute:",[100,653,655],{"className":102,"code":654,"language":104,"meta":105,"style":105},"class HttpClient:\n    retries: int\n    timeout: float\n\n    def __init__(self, retries: int = 3, timeout: float = 5.0) -> None:\n        self.retries = retries\n        self.timeout = timeout\n",[23,656,657,661,666,671,675,680,685],{"__ignoreMap":105},[109,658,659],{"class":111,"line":112},[109,660,150],{},[109,662,663],{"class":111,"line":118},[109,664,665],{},"    retries: int\n",[109,667,668],{"class":111,"line":124},[109,669,670],{},"    timeout: float\n",[109,672,673],{"class":111,"line":131},[109,674,128],{"emptyLinePlaceholder":127},[109,676,677],{"class":111,"line":137},[109,678,679],{},"    def __init__(self, retries: int = 3, timeout: float = 5.0) -> None:\n",[109,681,682],{"class":111,"line":142},[109,683,684],{},"        self.retries = retries\n",[109,686,687],{"class":111,"line":147},[109,688,689],{},"        self.timeout = timeout\n",[10,691,692,693,696,697,699,700,702,703,706],{},"Annotations without values do not create class attributes at runtime, but ",[23,694,695],{},"create_autospec"," reads them, so the spec now includes ",[23,698,29],{}," and ",[23,701,517],{},". The second fix is to spec from a real instance — ",[23,704,705],{},"create_autospec(HttpClient(), spec_set=True)"," — which sees the instance attributes directly, at the cost of constructing the real object. The third, for classes you do not own, is a Protocol or dataclass declaring the attributes the code reads, as with dynamic clients.",[258,708,710,789],{"className":709},[261],[263,711,271,716,271,719,271,722,271,725,271,728,271,732,271,735,271,740,271,744,271,747,271,750,271,753,271,755,271,758,271,762,271,765,271,769,271,773,271,775,271,779,271,783,271,786],{"viewBox":712,"role":266,"ariaLabelledBy":713,"xmlns":270},"0 0 800 234",[714,715],"inst-t","inst-d",[273,717,718],{"id":714},"Three ways to make instance attributes visible",[277,720,721],{"id":715},"Three options for attributes assigned only in init. Class-level annotations make them visible to a class-based spec at no runtime cost. Speccing from a real instance sees them directly but constructs the real object. A Protocol or dataclass declaring the attributes works for classes you do not own.",[281,723],{"x":283,"y":283,"width":561,"height":724,"rx":286,"fill":287},"234",[289,726,727],{"x":565,"y":292,"textAnchor":293,"fontSize":566,"fontWeight":295,"fill":296},"Give the spec the attributes it cannot otherwise see",[281,729],{"x":730,"y":570,"width":731,"height":572,"rx":573,"fill":334,"stroke":335,"strokeWidth":336},"24","240",[281,733],{"x":730,"y":570,"width":731,"height":734,"rx":573,"fill":296},"30",[289,736,739],{"x":737,"y":738,"textAnchor":293,"fontSize":573,"fontWeight":295,"fill":287},"144","70","class annotations",[289,741,743],{"x":742,"y":585,"fontSize":586,"fill":296},"40","retries: int",[289,745,746],{"x":742,"y":590,"fontSize":586,"fill":296},"no runtime cost",[289,748,749],{"x":742,"y":316,"fontSize":586,"fontWeight":295,"fill":348},"preferred",[281,751],{"x":752,"y":570,"width":731,"height":572,"rx":573,"fill":317,"stroke":318,"strokeWidth":336},"280",[281,754],{"x":752,"y":570,"width":731,"height":734,"rx":573,"fill":296},[289,756,757],{"x":565,"y":738,"textAnchor":293,"fontSize":573,"fontWeight":295,"fill":287},"spec an instance",[289,759,761],{"x":760,"y":585,"fontSize":586,"fill":296},"296","create_autospec(Cls())",[289,763,764],{"x":760,"y":590,"fontSize":586,"fill":296},"sees instance attributes",[289,766,768],{"x":760,"y":316,"fontSize":586,"fontWeight":295,"fill":767},"#8a5a00","needs a cheap constructor",[281,770],{"x":771,"y":570,"width":731,"height":572,"rx":573,"fill":305,"stroke":296,"strokeWidth":772},"536","1.6",[281,774],{"x":771,"y":570,"width":731,"height":734,"rx":573,"fill":296},[289,776,778],{"x":777,"y":738,"textAnchor":293,"fontSize":573,"fontWeight":295,"fill":287},"656","Protocol \u002F dataclass",[289,780,782],{"x":781,"y":585,"fontSize":586,"fill":296},"552","declare what you read",[289,784,785],{"x":781,"y":590,"fontSize":586,"fill":296},"for classes you don't own",[289,787,788],{"x":781,"y":316,"fontSize":586,"fontWeight":295,"fill":296},"verify against the real class",[395,790,791],{},"The first option also improves type checking of the production code, so it is rarely extra work in a codebase that already uses annotations.",[72,793,795],{"id":794},"rolling-it-out-across-a-suite","Rolling it out across a suite",[10,797,798,799,801,802,805],{},"Switching a large suite to ",[23,800,43],{}," is best done by collaborator rather than all at once. Start with the objects most often configured by attribute assignment — settings, clients with timeouts and retry counts, domain models whose status tests set — and change the fixtures that build their mocks to pass ",[23,803,804],{},"spec_set=True",". Run the suite; every failure is either a real stale attribute name, which gets fixed, or an instance attribute the spec cannot see, which gets a class annotation.",[10,807,808],{},"Because the change lives in the fixtures, individual tests do not need editing unless they were genuinely wrong. After a few collaborators the pattern is established and the remaining ones follow quickly, and from then on every rename in those classes is caught by the suite rather than discovered when a test that should have failed turns out to have been passing for months.",[72,810,812],{"id":811},"frequently-asked-questions","Frequently Asked Questions",[10,814,815,818,820,821,823,824,826,827,830,831,833,834,836],{},[440,816,817],{},"What is the difference between spec and spec_set?",[23,819,66],{}," restricts which attributes can be read from the mock; reading one the real object lacks raises ",[23,822,47],{},". ",[23,825,43],{}," additionally restricts which attributes can be assigned, so configuring ",[23,828,829],{},"mock.retrys = 3"," when the real attribute is ",[23,832,29],{}," also raises. ",[23,835,43],{}," catches drift in the test's own configuration.",[10,838,839,842,843,846,847,849],{},[440,840,841],{},"Does create_autospec use spec_set?","\nOnly if asked. ",[23,844,845],{},"create_autospec(Target, spec_set=True)"," enforces both signatures and assignment restrictions. Without ",[23,848,804],{},", autospecced mocks still allow arbitrary attribute assignment.",[10,851,852,855],{},[440,853,854],{},"Why would a test assign to a mock attribute at all?","\nTo configure state the code reads: a client's timeout, a config object's feature flags, a model's status. Those assignments are exactly where a rename in the real class goes unnoticed, because the test sets the old name and the code reads the new one.",[72,857,859],{"id":858},"related","Related",[77,861,862,868,878,888],{},[80,863,864,867],{},[90,865,866],{"href":92},"Autospec & Strict Mocking"," — the signature half of strictness.",[80,869,870,874,875,877],{},[90,871,873],{"href":872},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fautospec-strict-mocking\u002Fcreate-autospec-vs-patch-autospec-true\u002F","create_autospec vs patch(autospec=True)"," — where ",[23,876,43],{}," can be passed in each.",[80,879,880,884,885,887],{},[90,881,883],{"href":882},"\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 ",[23,886,43],{}," rejects.",[80,889,890,897],{},[90,891,893,894],{"href":892},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fautospec-strict-mocking\u002Fautospeccing-clients-that-use-getattr\u002F","Autospeccing Clients That Use ",[440,895,896],{},"getattr"," — when there is nothing concrete for the spec to see.",[10,899,900,901],{},"← Back to ",[90,902,866],{"href":92},[904,905,906],"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":105,"searchDepth":118,"depth":118,"links":908},[909,910,911,912,913,914,915,916,917],{"id":74,"depth":118,"text":75},{"id":97,"depth":118,"text":98},{"id":403,"depth":118,"text":404},{"id":433,"depth":118,"text":434},{"id":503,"depth":118,"text":504},{"id":624,"depth":118,"text":625},{"id":794,"depth":118,"text":795},{"id":811,"depth":118,"text":812},{"id":858,"depth":118,"text":859},"Use spec_set so mocks reject attribute assignments the real object would not accept, catching renamed attributes and misspelt configuration that spec and autospec allow.","md",{"slug":921,"type":922,"breadcrumb":43,"datePublished":923,"dateModified":923,"faq":924,"howto":931},"catching-signature-drift-with-spec-set","article","2026-09-18",[925,927,929],{"q":817,"a":926},"spec restricts which attributes can be read from the mock; reading one the real object lacks raises AttributeError. spec_set additionally restricts which attributes can be assigned, so configuring mock.retrys = 3 when the real attribute is retries also raises. spec_set catches drift in the test's own configuration.",{"q":841,"a":928},"Only if asked. create_autospec(Target, spec_set=True) enforces both signatures and assignment restrictions. Without spec_set=True, autospecced mocks still allow arbitrary attribute assignment.",{"q":854,"a":930},"To configure state the code reads: a client's timeout, a config object's feature flags, a model's status. Those assignments are exactly where a rename in the real class goes unnoticed, because the test sets the old name and the code reads the new one.",{"name":932,"description":933,"steps":934},"How to catch signature and attribute drift with spec_set","Create mocks with spec_set so both reads and writes are checked against the real object, and configure attributes that genuinely exist.",[935,938,941,944,947],{"name":936,"text":937},"Prefer create_autospec with spec_set","Use create_autospec(Target, instance=True, spec_set=True) for collaborators with configurable attributes.",{"name":939,"text":940},"Configure only real attributes","Assign values to attributes the real class defines; a misspelling now raises immediately.",{"name":942,"text":943},"Handle instance attributes set in __init__","Declare them as class-level annotations so the spec can see them.",{"name":945,"text":946},"Use it for configuration objects","Apply spec_set to settings and feature-flag objects, where renamed fields drift most often.",{"name":948,"text":949},"Let the failure point at the rename","Treat an AttributeError on assignment as a signal the real class changed and update the test to match.","\u002Fadvanced-mocking-test-doubles-in-python\u002Fautospec-strict-mocking\u002Fcatching-signature-drift-with-spec-set",{"title":5,"description":918},"advanced-mocking-test-doubles-in-python\u002Fautospec-strict-mocking\u002Fcatching-signature-drift-with-spec-set\u002Findex","bfTq4AQmIvmPUTgMidQIPwRuHzmljbyc16h8simVOhA",1789718767589]