[{"data":1,"prerenderedAt":2396},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-cleaning-code-with-hypothesis":3,"surround:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-cleaning-code-with-hypothesis":2361,"ancestors:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-cleaning-code-with-hypothesis":2370,"children:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-cleaning-code-with-hypothesis":2375,"related:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-cleaning-code-with-hypothesis":2376},{"id":4,"title":5,"body":6,"dateModified":2327,"datePublished":2327,"description":2328,"extension":2329,"faq":2330,"keyword":2339,"kind":2340,"meta":2341,"navigation":234,"path":2350,"seo":2351,"slug":2354,"stem":2355,"topics":2356,"type":2359,"__hash__":2360},"docs\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-cleaning-code-with-hypothesis\u002Findex.md","Test Excel Cleaning Code with Hypothesis",{"type":7,"value":8,"toc":2315},"minimark",[9,24,29,67,78,181,185,192,450,488,501,505,508,742,785,792,812,979,983,986,1341,1391,1431,1435,1441,1867,1897,1908,1912,1919,2015,2055,2166,2184,2188,2233,2237,2246,2250,2256,2266,2272,2278,2282,2311],[10,11,12,13,17,18,23],"p",{},"Example-based tests check the inputs you thought of: a name with two spaces, a price with a comma, the file from last month. Real spreadsheets contain the inputs you did not think of — a tab pasted into a cell, a zero-width space, a number with seventeen significant digits, a cell that is empty but not missing. ",[14,15,16],"em",{},"Property-based"," testing flips the approach: you state what must always be true of the output, and Hypothesis generates hundreds of inputs trying to break it, then shrinks any failure to the smallest example that still fails. For code that cleans and writes Excel data, it finds real problems within seconds. This page, part of ",[19,20,22],"a",{"href":21},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002F","Testing and Packaging Excel Automation Scripts",", shows three that it found while writing it.",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install hypothesis pytest pandas openpyxl xlsxwriter\n","bash","",[37,38,39],"code",{"__ignoreMap":35},[40,41,44,48,52,55,58,61,64],"span",{"class":42,"line":43},"line",1,[40,45,47],{"class":46},"sMTad","pip",[40,49,51],{"class":50},"srMev"," install",[40,53,54],{"class":50}," hypothesis",[40,56,57],{"class":50}," pytest",[40,59,60],{"class":50}," pandas",[40,62,63],{"class":50}," openpyxl",[40,65,66],{"class":50}," xlsxwriter\n",[10,68,69,70,73,74,77],{},"The tests below are ordinary pytest files, run with ",[37,71,72],{},"pytest",". Every test sets ",[37,75,76],{},"derandomize=True",", which makes Hypothesis generate the same sequence of examples on every run, so the failures shown here are reproducible — and so a CI run is not flaky. The failing examples quoted on this page are exactly what Hypothesis reported.",[79,80,88,89,88,93,88,97,88,105,88,115,88,126,88,133,88,139,88,144,88,148,88,152,88,157,88,164,88,168,88,172,88,175,88,178],"svg",{"viewBox":81,"role":82,"ariaLabelledBy":83,"xmlns":86,"style":87},"4 0 752 207","img",[84,85],"hy-idea-t","hy-idea-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[90,91,92],"title",{"id":84},"Example tests and property tests",[94,95,96],"desc",{"id":85},"Two panels. Example tests check specific inputs chosen by the author and their exact outputs. Property tests state rules that must hold for every input and let Hypothesis generate and shrink inputs to find a counterexample.",[98,99],"rect",{"x":100,"y":101,"width":102,"height":103,"fill":104},"4","0","752","207","#ffffff",[98,106],{"x":107,"y":108,"width":109,"height":110,"rx":111,"fill":112,"stroke":113,"style":114},"20.0","16.0","351.0","175.0","14","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[116,117,119],"g",{"transform":118},"translate(182.5,29.0) scale(1.083)",[120,121],"path",{"d":122,"fill":123,"stroke":124,"style":125},"M5 12.5l4.5 4.5L19 7","none","var(--text,#172033)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[127,128,132],"text",{"x":129,"y":130,"style":131},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Example-based",[42,134],{"x1":135,"y1":136,"x2":137,"y2":136,"stroke":113,"style":138},"36.0","91.0","355.0","stroke-width:1px",[127,140,143],{"x":129,"y":141,"style":142},"111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","inputs you thought of",[127,145,147],{"x":129,"y":146,"style":142},"137.0","exact expected output",[127,149,151],{"x":129,"y":150,"style":142},"163.0","misses the surprising cases",[98,153],{"x":154,"y":108,"width":109,"height":110,"rx":111,"fill":155,"stroke":156,"style":114},"389.0","#d9f4f1","var(--teal,#0f9488)",[116,158,160],{"transform":159},"translate(551.5,29.0) scale(1.083)",[120,161],{"d":162,"fill":123,"stroke":163,"style":125},"M11 4a7 7 0 1 0 .01 0z M16 16l5 5","var(--teal-ink,#0b6157)",[127,165,16],{"x":166,"y":130,"style":167},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[42,169],{"x1":170,"y1":136,"x2":171,"y2":136,"stroke":156,"style":138},"405.0","724.0",[127,173,174],{"x":166,"y":141,"style":142},"rules that always hold",[127,176,177],{"x":166,"y":146,"style":142},"hundreds of generated inputs",[127,179,180],{"x":166,"y":150,"style":142},"failures shrunk to a minimal example",[25,182,184],{"id":183},"step-1-properties-of-a-cleaning-function","Step 1 — Properties of a cleaning function",[10,186,187,188,191],{},"A typical cleaning function normalises names from a spreadsheet column: collapse runs of whitespace, trim the ends, title-case. Instead of listing examples, state properties it must have for ",[14,189,190],{},"any"," text — no double spaces, no leading or trailing whitespace, and running it twice changes nothing:",[30,193,197],{"className":194,"code":195,"language":196,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","import re\nfrom hypothesis import given, settings, strategies as st\n\ndef clean_name(s: str) -> str:\n    return re.sub(r\"\\s+\", \" \", s).strip().title()\n\n@settings(derandomize=True, max_examples=500)\n@given(st.text())\ndef test_no_double_spaces_or_padding(s):\n    out = clean_name(s)\n    assert \"  \" not in out\n    assert out == out.strip()\n\n@settings(derandomize=True, max_examples=500)\n@given(st.text())\ndef test_idempotent(s):\n    assert clean_name(clean_name(s)) == clean_name(s)\n","python",[37,198,199,209,229,236,261,293,298,330,339,350,361,379,393,398,421,428,438],{"__ignoreMap":35},[40,200,201,205],{"class":42,"line":43},[40,202,204],{"class":203},"s-kum","import",[40,206,208],{"class":207},"skGVy"," re\n",[40,210,212,215,218,220,223,226],{"class":42,"line":211},2,[40,213,214],{"class":203},"from",[40,216,217],{"class":207}," hypothesis ",[40,219,204],{"class":203},[40,221,222],{"class":207}," given, settings, strategies ",[40,224,225],{"class":203},"as",[40,227,228],{"class":207}," st\n",[40,230,232],{"class":42,"line":231},3,[40,233,235],{"emptyLinePlaceholder":234},true,"\n",[40,237,239,242,246,249,253,256,258],{"class":42,"line":238},4,[40,240,241],{"class":203},"def",[40,243,245],{"class":244},"s_Opv"," clean_name",[40,247,248],{"class":207},"(s: ",[40,250,252],{"class":251},"sP0c6","str",[40,254,255],{"class":207},") -> ",[40,257,252],{"class":251},[40,259,260],{"class":207},":\n",[40,262,264,267,270,273,276,279,282,284,287,290],{"class":42,"line":263},5,[40,265,266],{"class":203},"    return",[40,268,269],{"class":207}," re.sub(",[40,271,272],{"class":203},"r",[40,274,275],{"class":50},"\"",[40,277,278],{"class":251},"\\s",[40,280,281],{"class":203},"+",[40,283,275],{"class":50},[40,285,286],{"class":207},", ",[40,288,289],{"class":50},"\" \"",[40,291,292],{"class":207},", s).strip().title()\n",[40,294,296],{"class":42,"line":295},6,[40,297,235],{"emptyLinePlaceholder":234},[40,299,301,304,307,311,314,317,319,322,324,327],{"class":42,"line":300},7,[40,302,303],{"class":244},"@settings",[40,305,306],{"class":207},"(",[40,308,310],{"class":309},"sa561","derandomize",[40,312,313],{"class":203},"=",[40,315,316],{"class":251},"True",[40,318,286],{"class":207},[40,320,321],{"class":309},"max_examples",[40,323,313],{"class":203},[40,325,326],{"class":251},"500",[40,328,329],{"class":207},")\n",[40,331,333,336],{"class":42,"line":332},8,[40,334,335],{"class":244},"@given",[40,337,338],{"class":207},"(st.text())\n",[40,340,342,344,347],{"class":42,"line":341},9,[40,343,241],{"class":203},[40,345,346],{"class":244}," test_no_double_spaces_or_padding",[40,348,349],{"class":207},"(s):\n",[40,351,353,356,358],{"class":42,"line":352},10,[40,354,355],{"class":207},"    out ",[40,357,313],{"class":203},[40,359,360],{"class":207}," clean_name(s)\n",[40,362,364,367,370,373,376],{"class":42,"line":363},11,[40,365,366],{"class":203},"    assert",[40,368,369],{"class":50}," \"  \"",[40,371,372],{"class":203}," not",[40,374,375],{"class":203}," in",[40,377,378],{"class":207}," out\n",[40,380,382,384,387,390],{"class":42,"line":381},12,[40,383,366],{"class":203},[40,385,386],{"class":207}," out ",[40,388,389],{"class":203},"==",[40,391,392],{"class":207}," out.strip()\n",[40,394,396],{"class":42,"line":395},13,[40,397,235],{"emptyLinePlaceholder":234},[40,399,401,403,405,407,409,411,413,415,417,419],{"class":42,"line":400},14,[40,402,303],{"class":244},[40,404,306],{"class":207},[40,406,310],{"class":309},[40,408,313],{"class":203},[40,410,316],{"class":251},[40,412,286],{"class":207},[40,414,321],{"class":309},[40,416,313],{"class":203},[40,418,326],{"class":251},[40,420,329],{"class":207},[40,422,424,426],{"class":42,"line":423},15,[40,425,335],{"class":244},[40,427,338],{"class":207},[40,429,431,433,436],{"class":42,"line":430},16,[40,432,241],{"class":203},[40,434,435],{"class":244}," test_idempotent",[40,437,349],{"class":207},[40,439,441,443,446,448],{"class":42,"line":440},17,[40,442,366],{"class":203},[40,444,445],{"class":207}," clean_name(clean_name(s)) ",[40,447,389],{"class":203},[40,449,360],{"class":207},[30,451,453],{"className":32,"code":452,"language":34,"meta":35,"style":35},"python -m pytest -q -p no:cacheprovider test_clean.py 2>&1 | tail -1\n",[37,454,455],{"__ignoreMap":35},[40,456,457,459,462,464,467,470,473,476,479,482,485],{"class":42,"line":43},[40,458,196],{"class":46},[40,460,461],{"class":251}," -m",[40,463,57],{"class":50},[40,465,466],{"class":251}," -q",[40,468,469],{"class":251}," -p",[40,471,472],{"class":50}," no:cacheprovider",[40,474,475],{"class":50}," test_clean.py",[40,477,478],{"class":203}," 2>&1",[40,480,481],{"class":203}," |",[40,483,484],{"class":46}," tail",[40,486,487],{"class":251}," -1\n",[10,489,490,493,494,496,497,500],{},[37,491,492],{},"st.text()"," generates arbitrary Unicode strings — non-breaking spaces, tabs, line separators, combining accents — far beyond what anyone would type as test data. Both properties hold, because ",[37,495,278],{}," and ",[37,498,499],{},"strip()"," both understand Unicode whitespace. Idempotence is a particularly useful property for cleaning steps: a pipeline that cleans already-cleaned data must not change it again, which matters when a job is rerun on its own output.",[25,502,504],{"id":503},"step-2-a-round-trip-property-finds-three-real-problems","Step 2 — A round-trip property finds three real problems",[10,506,507],{},"The most valuable property for Excel code is the round trip: whatever you write to a workbook should come back unchanged when you read it. State it for a column of text and let Hypothesis look for exceptions:",[30,509,511],{"className":194,"code":510,"language":196,"meta":35,"style":35},"import io\nimport pandas as pd\nfrom hypothesis import HealthCheck, given, settings, strategies as st\n\nSLOW = [HealthCheck.too_slow]\n\ndef roundtrip(df):\n    buf = io.BytesIO()\n    df.to_excel(buf, index=False)\n    buf.seek(0)\n    return pd.read_excel(buf, dtype=str, keep_default_na=False)\n\n@settings(derandomize=True, max_examples=200, suppress_health_check=SLOW)\n@given(st.lists(st.text(max_size=20), min_size=1, max_size=5))\ndef test_text_roundtrip(values):\n    assert roundtrip(pd.DataFrame({\"name\": values}))[\"name\"].tolist() == values\n",[37,512,513,520,532,547,551,562,566,576,586,601,610,635,639,671,709,719],{"__ignoreMap":35},[40,514,515,517],{"class":42,"line":43},[40,516,204],{"class":203},[40,518,519],{"class":207}," io\n",[40,521,522,524,527,529],{"class":42,"line":211},[40,523,204],{"class":203},[40,525,526],{"class":207}," pandas ",[40,528,225],{"class":203},[40,530,531],{"class":207}," pd\n",[40,533,534,536,538,540,543,545],{"class":42,"line":231},[40,535,214],{"class":203},[40,537,217],{"class":207},[40,539,204],{"class":203},[40,541,542],{"class":207}," HealthCheck, given, settings, strategies ",[40,544,225],{"class":203},[40,546,228],{"class":207},[40,548,549],{"class":42,"line":238},[40,550,235],{"emptyLinePlaceholder":234},[40,552,553,556,559],{"class":42,"line":263},[40,554,555],{"class":251},"SLOW",[40,557,558],{"class":203}," =",[40,560,561],{"class":207}," [HealthCheck.too_slow]\n",[40,563,564],{"class":42,"line":295},[40,565,235],{"emptyLinePlaceholder":234},[40,567,568,570,573],{"class":42,"line":300},[40,569,241],{"class":203},[40,571,572],{"class":244}," roundtrip",[40,574,575],{"class":207},"(df):\n",[40,577,578,581,583],{"class":42,"line":332},[40,579,580],{"class":207},"    buf ",[40,582,313],{"class":203},[40,584,585],{"class":207}," io.BytesIO()\n",[40,587,588,591,594,596,599],{"class":42,"line":341},[40,589,590],{"class":207},"    df.to_excel(buf, ",[40,592,593],{"class":309},"index",[40,595,313],{"class":203},[40,597,598],{"class":251},"False",[40,600,329],{"class":207},[40,602,603,606,608],{"class":42,"line":352},[40,604,605],{"class":207},"    buf.seek(",[40,607,101],{"class":251},[40,609,329],{"class":207},[40,611,612,614,617,620,622,624,626,629,631,633],{"class":42,"line":363},[40,613,266],{"class":203},[40,615,616],{"class":207}," pd.read_excel(buf, ",[40,618,619],{"class":309},"dtype",[40,621,313],{"class":203},[40,623,252],{"class":251},[40,625,286],{"class":207},[40,627,628],{"class":309},"keep_default_na",[40,630,313],{"class":203},[40,632,598],{"class":251},[40,634,329],{"class":207},[40,636,637],{"class":42,"line":381},[40,638,235],{"emptyLinePlaceholder":234},[40,640,641,643,645,647,649,651,653,655,657,660,662,665,667,669],{"class":42,"line":395},[40,642,303],{"class":244},[40,644,306],{"class":207},[40,646,310],{"class":309},[40,648,313],{"class":203},[40,650,316],{"class":251},[40,652,286],{"class":207},[40,654,321],{"class":309},[40,656,313],{"class":203},[40,658,659],{"class":251},"200",[40,661,286],{"class":207},[40,663,664],{"class":309},"suppress_health_check",[40,666,313],{"class":203},[40,668,555],{"class":251},[40,670,329],{"class":207},[40,672,673,675,678,681,683,686,689,692,694,697,699,701,703,706],{"class":42,"line":400},[40,674,335],{"class":244},[40,676,677],{"class":207},"(st.lists(st.text(",[40,679,680],{"class":309},"max_size",[40,682,313],{"class":203},[40,684,685],{"class":251},"20",[40,687,688],{"class":207},"), ",[40,690,691],{"class":309},"min_size",[40,693,313],{"class":203},[40,695,696],{"class":251},"1",[40,698,286],{"class":207},[40,700,680],{"class":309},[40,702,313],{"class":203},[40,704,705],{"class":251},"5",[40,707,708],{"class":207},"))\n",[40,710,711,713,716],{"class":42,"line":423},[40,712,241],{"class":203},[40,714,715],{"class":244}," test_text_roundtrip",[40,717,718],{"class":207},"(values):\n",[40,720,721,723,726,729,732,734,737,739],{"class":42,"line":430},[40,722,366],{"class":203},[40,724,725],{"class":207}," roundtrip(pd.DataFrame({",[40,727,728],{"class":50},"\"name\"",[40,730,731],{"class":207},": values}))[",[40,733,728],{"class":50},[40,735,736],{"class":207},"].tolist() ",[40,738,389],{"class":203},[40,740,741],{"class":207}," values\n",[30,743,745],{"className":32,"code":744,"language":34,"meta":35,"style":35},"python -m pytest -q -p no:cacheprovider test_roundtrip.py 2>&1 | grep -E \"values=|AssertionError|failed|passed\" | head -4\n",[37,746,747],{"__ignoreMap":35},[40,748,749,751,753,755,757,759,761,764,766,768,771,774,777,779,782],{"class":42,"line":43},[40,750,196],{"class":46},[40,752,461],{"class":251},[40,754,57],{"class":50},[40,756,466],{"class":251},[40,758,469],{"class":251},[40,760,472],{"class":50},[40,762,763],{"class":50}," test_roundtrip.py",[40,765,478],{"class":203},[40,767,481],{"class":203},[40,769,770],{"class":46}," grep",[40,772,773],{"class":251}," -E",[40,775,776],{"class":50}," \"values=|AssertionError|failed|passed\"",[40,778,481],{"class":203},[40,780,781],{"class":46}," head",[40,783,784],{"class":251}," -4\n",[10,786,787,788,791],{},"Hypothesis fails the test with the smallest example it can find: ",[37,789,790],{},"values=['']",". A DataFrame with one empty string writes a header and a row whose only cell is empty — and an empty cell is not stored in the file at all, so reading back finds no data row. In a real report, a row of blank text cells at the end of a sheet disappears the same way. Excel has no difference between \"empty string\" and \"no value\"; decide which one your data means and convert explicitly before writing.",[10,793,794,795,798,799,802,803,806,807,811],{},"With empty strings excluded (",[37,796,797],{},"min_size=1","), the next run finds ",[37,800,801],{},"values=['\\x1f']",", a control character, which comes back as the seven characters ",[37,804,805],{},"_x001F_",". The default engine, xlsxwriter, escapes control characters in the XML that way, and openpyxl returns the escape sequence as literal text when reading. Data from APIs and pasted text contains such characters more often than one would think; strip them before writing — ",[19,808,810],{"href":809},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fremove-non-printable-characters-from-excel-data\u002F","Remove Non-Printable Characters from Excel Data"," shows how.",[79,813,88,818,88,821,88,824,88,827,88,834,88,841,88,849,88,855,88,860,88,864,88,868,88,872,88,876,88,881,88,886,88,890,88,893,88,895,88,897,88,902,88,908,88,914,88,917,88,921,88,925,88,927,88,929,88,932,88,937,88,941,88,945,88,949,88,953,88,955,88,957,88,960,88,965,88,971,88,975,88,977],{"viewBox":814,"role":82,"ariaLabelledBy":815,"xmlns":86,"style":87},"4 0 752 346",[816,817],"hy-finds-t","hy-finds-d",[90,819,820],{"id":816},"What Hypothesis found, run after run",[94,822,823],{"id":817},"A timeline of test runs. The first found that an empty string vanishes, the second that a control character comes back as _x001F_, and the float run that numbers are rounded to 15 or 16 digits and that a value near the float maximum crashes the reader. After handling these the properties passed.",[98,825],{"x":100,"y":101,"width":102,"height":826,"fill":104},"346",[42,828],{"x1":829,"y1":830,"x2":831,"y2":830,"stroke":832,"style":833},"76.0","173.0","684.0","var(--muted,#5b6780)","stroke-width:2.5px",[98,835],{"x":107,"y":108,"width":836,"height":837,"rx":838,"fill":839,"stroke":840,"style":114},"136.0","125.0","12","#fce9e9","var(--danger,#dc2626)",[116,842,844],{"transform":843},"translate(76.0,37.2) scale(1.000)",[120,845],{"d":846,"fill":123,"stroke":847,"style":848},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--accent-ink,#be185d)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[127,850,854],{"x":851,"y":852,"style":853},"88.0","82.8","font-size:12.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","run 1: ['']",[127,856,859],{"x":851,"y":857,"style":858},"101.2","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","empty string",[127,861,863],{"x":851,"y":862,"style":858},"118.2","vanishes",[42,865],{"x1":851,"y1":866,"x2":851,"y2":830,"stroke":832,"style":867},"141.0","stroke-width:1.5px",[869,870],"circle",{"cx":851,"cy":830,"r":871,"fill":840,"stroke":104,"style":114},"7.0",[98,873],{"x":874,"y":875,"width":836,"height":837,"rx":838,"fill":839,"stroke":840,"style":114},"166.0","205.0",[116,877,879],{"transform":878},"translate(222.0,226.2) scale(1.000)",[120,880],{"d":846,"fill":123,"stroke":847,"style":848},[127,882,885],{"x":883,"y":884,"style":853},"234.0","271.8","run 2: ['\\x1f']",[127,887,889],{"x":883,"y":888,"style":858},"290.2","comes back as",[127,891,805],{"x":883,"y":892,"style":858},"307.2",[42,894],{"x1":883,"y1":830,"x2":883,"y2":875,"stroke":832,"style":867},[869,896],{"cx":883,"cy":830,"r":871,"fill":840,"stroke":104,"style":114},[98,898],{"x":899,"y":108,"width":836,"height":837,"rx":838,"fill":900,"stroke":901,"style":114},"312.0","#fdefd8","var(--gold,#b4740a)",[116,903,905],{"transform":904},"translate(368.0,29.0) scale(1.000)",[120,906],{"d":846,"fill":123,"stroke":907,"style":848},"var(--gold-ink,#7a4e06)",[127,909,913],{"x":910,"y":911,"style":912},"380.0","74.5","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","run 3:",[127,915,916],{"x":910,"y":136,"style":912},"2.105…227e20",[127,918,920],{"x":910,"y":919,"style":858},"109.5","rounded to 15–16",[127,922,924],{"x":910,"y":923,"style":858},"126.5","digits",[42,926],{"x1":910,"y1":866,"x2":910,"y2":830,"stroke":832,"style":867},[869,928],{"cx":910,"cy":830,"r":871,"fill":901,"stroke":104,"style":114},[98,930],{"x":931,"y":875,"width":836,"height":837,"rx":838,"fill":839,"stroke":840,"style":114},"458.0",[116,933,935],{"transform":934},"translate(514.0,218.0) scale(1.000)",[120,936],{"d":846,"fill":123,"stroke":847,"style":848},[127,938,913],{"x":939,"y":940,"style":853},"526.0","263.5",[127,942,944],{"x":939,"y":943,"style":853},"280.0","−1.797…155e308",[127,946,948],{"x":939,"y":947,"style":858},"298.5","reading it back",[127,950,952],{"x":939,"y":951,"style":858},"315.5","crashes",[42,954],{"x1":939,"y1":830,"x2":939,"y2":875,"stroke":832,"style":867},[869,956],{"cx":939,"cy":830,"r":871,"fill":840,"stroke":104,"style":114},[98,958],{"x":959,"y":108,"width":836,"height":837,"rx":838,"fill":155,"stroke":156,"style":114},"604.0",[116,961,963],{"transform":962},"translate(660.0,45.8) scale(1.000)",[120,964],{"d":122,"fill":123,"stroke":163,"style":848},[127,966,970],{"x":967,"y":968,"style":969},"672.0","91.2","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","run 4",[127,972,974],{"x":967,"y":973,"style":858},"109.8","property holds",[42,976],{"x1":967,"y1":866,"x2":967,"y2":830,"stroke":832,"style":867},[869,978],{"cx":967,"cy":830,"r":871,"fill":156,"stroke":104,"style":114},[25,980,982],{"id":981},"step-3-numbers-the-precision-limit","Step 3 — Numbers: the precision limit",[10,984,985],{},"Run the same property for floats, excluding NaN and infinity, which Excel cannot store:",[30,987,989],{"className":194,"code":988,"language":196,"meta":35,"style":35},"import io\nimport math\nimport pandas as pd\nfrom hypothesis import HealthCheck, given, settings, strategies as st\n\ndef roundtrip(values):\n    buf = io.BytesIO()\n    pd.DataFrame({\"x\": values}).to_excel(buf, index=False)\n    buf.seek(0)\n    return pd.read_excel(buf)[\"x\"].tolist()\n\nfloats = st.floats(allow_nan=False, allow_infinity=False)\namounts = st.floats(min_value=-1e15, max_value=1e15)\n\n@settings(derandomize=True, max_examples=150, suppress_health_check=[HealthCheck.too_slow])\n@given(st.lists(floats, min_size=1, max_size=3))\ndef test_exact(values):\n    assert roundtrip(values) == values\n\n@settings(derandomize=True, max_examples=150, suppress_health_check=[HealthCheck.too_slow])\n@given(st.lists(amounts, min_size=1, max_size=3))\ndef test_close(values):\n    assert all(math.isclose(a, b, rel_tol=1e-14, abs_tol=0) for a, b in zip(roundtrip(values), values))\n",[37,990,991,997,1004,1014,1028,1032,1040,1048,1067,1075,1087,1091,1119,1148,1152,1182,1206,1215,1227,1232,1261,1285,1295],{"__ignoreMap":35},[40,992,993,995],{"class":42,"line":43},[40,994,204],{"class":203},[40,996,519],{"class":207},[40,998,999,1001],{"class":42,"line":211},[40,1000,204],{"class":203},[40,1002,1003],{"class":207}," math\n",[40,1005,1006,1008,1010,1012],{"class":42,"line":231},[40,1007,204],{"class":203},[40,1009,526],{"class":207},[40,1011,225],{"class":203},[40,1013,531],{"class":207},[40,1015,1016,1018,1020,1022,1024,1026],{"class":42,"line":238},[40,1017,214],{"class":203},[40,1019,217],{"class":207},[40,1021,204],{"class":203},[40,1023,542],{"class":207},[40,1025,225],{"class":203},[40,1027,228],{"class":207},[40,1029,1030],{"class":42,"line":263},[40,1031,235],{"emptyLinePlaceholder":234},[40,1033,1034,1036,1038],{"class":42,"line":295},[40,1035,241],{"class":203},[40,1037,572],{"class":244},[40,1039,718],{"class":207},[40,1041,1042,1044,1046],{"class":42,"line":300},[40,1043,580],{"class":207},[40,1045,313],{"class":203},[40,1047,585],{"class":207},[40,1049,1050,1053,1056,1059,1061,1063,1065],{"class":42,"line":332},[40,1051,1052],{"class":207},"    pd.DataFrame({",[40,1054,1055],{"class":50},"\"x\"",[40,1057,1058],{"class":207},": values}).to_excel(buf, ",[40,1060,593],{"class":309},[40,1062,313],{"class":203},[40,1064,598],{"class":251},[40,1066,329],{"class":207},[40,1068,1069,1071,1073],{"class":42,"line":341},[40,1070,605],{"class":207},[40,1072,101],{"class":251},[40,1074,329],{"class":207},[40,1076,1077,1079,1082,1084],{"class":42,"line":352},[40,1078,266],{"class":203},[40,1080,1081],{"class":207}," pd.read_excel(buf)[",[40,1083,1055],{"class":50},[40,1085,1086],{"class":207},"].tolist()\n",[40,1088,1089],{"class":42,"line":363},[40,1090,235],{"emptyLinePlaceholder":234},[40,1092,1093,1096,1098,1101,1104,1106,1108,1110,1113,1115,1117],{"class":42,"line":381},[40,1094,1095],{"class":207},"floats ",[40,1097,313],{"class":203},[40,1099,1100],{"class":207}," st.floats(",[40,1102,1103],{"class":309},"allow_nan",[40,1105,313],{"class":203},[40,1107,598],{"class":251},[40,1109,286],{"class":207},[40,1111,1112],{"class":309},"allow_infinity",[40,1114,313],{"class":203},[40,1116,598],{"class":251},[40,1118,329],{"class":207},[40,1120,1121,1124,1126,1128,1131,1134,1137,1139,1142,1144,1146],{"class":42,"line":395},[40,1122,1123],{"class":207},"amounts ",[40,1125,313],{"class":203},[40,1127,1100],{"class":207},[40,1129,1130],{"class":309},"min_value",[40,1132,1133],{"class":203},"=-",[40,1135,1136],{"class":251},"1e15",[40,1138,286],{"class":207},[40,1140,1141],{"class":309},"max_value",[40,1143,313],{"class":203},[40,1145,1136],{"class":251},[40,1147,329],{"class":207},[40,1149,1150],{"class":42,"line":400},[40,1151,235],{"emptyLinePlaceholder":234},[40,1153,1154,1156,1158,1160,1162,1164,1166,1168,1170,1173,1175,1177,1179],{"class":42,"line":423},[40,1155,303],{"class":244},[40,1157,306],{"class":207},[40,1159,310],{"class":309},[40,1161,313],{"class":203},[40,1163,316],{"class":251},[40,1165,286],{"class":207},[40,1167,321],{"class":309},[40,1169,313],{"class":203},[40,1171,1172],{"class":251},"150",[40,1174,286],{"class":207},[40,1176,664],{"class":309},[40,1178,313],{"class":203},[40,1180,1181],{"class":207},"[HealthCheck.too_slow])\n",[40,1183,1184,1186,1189,1191,1193,1195,1197,1199,1201,1204],{"class":42,"line":430},[40,1185,335],{"class":244},[40,1187,1188],{"class":207},"(st.lists(floats, ",[40,1190,691],{"class":309},[40,1192,313],{"class":203},[40,1194,696],{"class":251},[40,1196,286],{"class":207},[40,1198,680],{"class":309},[40,1200,313],{"class":203},[40,1202,1203],{"class":251},"3",[40,1205,708],{"class":207},[40,1207,1208,1210,1213],{"class":42,"line":440},[40,1209,241],{"class":203},[40,1211,1212],{"class":244}," test_exact",[40,1214,718],{"class":207},[40,1216,1218,1220,1223,1225],{"class":42,"line":1217},18,[40,1219,366],{"class":203},[40,1221,1222],{"class":207}," roundtrip(values) ",[40,1224,389],{"class":203},[40,1226,741],{"class":207},[40,1228,1230],{"class":42,"line":1229},19,[40,1231,235],{"emptyLinePlaceholder":234},[40,1233,1235,1237,1239,1241,1243,1245,1247,1249,1251,1253,1255,1257,1259],{"class":42,"line":1234},20,[40,1236,303],{"class":244},[40,1238,306],{"class":207},[40,1240,310],{"class":309},[40,1242,313],{"class":203},[40,1244,316],{"class":251},[40,1246,286],{"class":207},[40,1248,321],{"class":309},[40,1250,313],{"class":203},[40,1252,1172],{"class":251},[40,1254,286],{"class":207},[40,1256,664],{"class":309},[40,1258,313],{"class":203},[40,1260,1181],{"class":207},[40,1262,1264,1266,1269,1271,1273,1275,1277,1279,1281,1283],{"class":42,"line":1263},21,[40,1265,335],{"class":244},[40,1267,1268],{"class":207},"(st.lists(amounts, ",[40,1270,691],{"class":309},[40,1272,313],{"class":203},[40,1274,696],{"class":251},[40,1276,286],{"class":207},[40,1278,680],{"class":309},[40,1280,313],{"class":203},[40,1282,1203],{"class":251},[40,1284,708],{"class":207},[40,1286,1288,1290,1293],{"class":42,"line":1287},22,[40,1289,241],{"class":203},[40,1291,1292],{"class":244}," test_close",[40,1294,718],{"class":207},[40,1296,1298,1300,1303,1306,1309,1311,1314,1316,1319,1321,1323,1326,1329,1332,1335,1338],{"class":42,"line":1297},23,[40,1299,366],{"class":203},[40,1301,1302],{"class":251}," all",[40,1304,1305],{"class":207},"(math.isclose(a, b, ",[40,1307,1308],{"class":309},"rel_tol",[40,1310,313],{"class":203},[40,1312,1313],{"class":251},"1e-14",[40,1315,286],{"class":207},[40,1317,1318],{"class":309},"abs_tol",[40,1320,313],{"class":203},[40,1322,101],{"class":251},[40,1324,1325],{"class":207},") ",[40,1327,1328],{"class":203},"for",[40,1330,1331],{"class":207}," a, b ",[40,1333,1334],{"class":203},"in",[40,1336,1337],{"class":251}," zip",[40,1339,1340],{"class":207},"(roundtrip(values), values))\n",[30,1342,1344],{"className":32,"code":1343,"language":34,"meta":35,"style":35},"python -m pytest -q -p no:cacheprovider test_numbers.py 2>&1 | grep -E \"values=|failed|passed\" | head -3\npython -c \"import io, pandas as pd; b = io.BytesIO(); pd.DataFrame({'x': [0.1 + 0.2]}).to_excel(b, index=False); b.seek(0); print(repr(0.1 + 0.2), '->', pd.read_excel(b)['x'][0])\"\n",[37,1345,1346,1381],{"__ignoreMap":35},[40,1347,1348,1350,1352,1354,1356,1358,1360,1363,1365,1367,1369,1371,1374,1376,1378],{"class":42,"line":43},[40,1349,196],{"class":46},[40,1351,461],{"class":251},[40,1353,57],{"class":50},[40,1355,466],{"class":251},[40,1357,469],{"class":251},[40,1359,472],{"class":50},[40,1361,1362],{"class":50}," test_numbers.py",[40,1364,478],{"class":203},[40,1366,481],{"class":203},[40,1368,770],{"class":46},[40,1370,773],{"class":251},[40,1372,1373],{"class":50}," \"values=|failed|passed\"",[40,1375,481],{"class":203},[40,1377,781],{"class":46},[40,1379,1380],{"class":251}," -3\n",[40,1382,1383,1385,1388],{"class":42,"line":211},[40,1384,196],{"class":46},[40,1386,1387],{"class":251}," -c",[40,1389,1390],{"class":50}," \"import io, pandas as pd; b = io.BytesIO(); pd.DataFrame({'x': [0.1 + 0.2]}).to_excel(b, index=False); b.seek(0); print(repr(0.1 + 0.2), '->', pd.read_excel(b)['x'][0])\"\n",[10,1392,1393,1394,1397,1398,1401,1402,1405,1406,1409,1410,1413,1414,1417,1418,1421,1422,1425,1426,1430],{},"The exact test fails twice over. The first failure is precision: xlsxwriter writes numbers with the 15–16 significant digits Excel itself works with, so ",[37,1395,1396],{},"2.1050057130643227e+20"," is stored as ",[37,1399,1400],{},"2.105005713064323E+20"," and comes back slightly different — and ",[37,1403,1404],{},"0.30000000000000004"," becomes ",[37,1407,1408],{},"0.3",", which is usually what people wanted anyway. The second is more alarming: a value at the very edge of the float range, ",[37,1411,1412],{},"-1.7976931348623155e+308",", is written rounded away from zero to ",[37,1415,1416],{},"-1.797693134862316E+308",", which is beyond the largest magnitude a Python float can hold, so reading it back makes pandas' reader crash with ",[37,1419,1420],{},"OverflowError",". No real report contains such a number — but a sentinel value like ",[37,1423,1424],{},"sys.float_info.max"," used to mean \"no limit\" does. The tolerance test, restricted to amounts up to a quadrillion, passes. The property is now honest about the format's limit, and a value that must survive exactly — a 17-digit ID, a cryptographic hash — has to be written as text, as ",[19,1427,1429],{"href":1428},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-long-id-numbers-losing-digits-in-excel\u002F","Fix Long ID Numbers Losing Digits in Excel"," explains.",[25,1432,1434],{"id":1433},"step-4-generate-whole-dataframes","Step 4 — Generate whole DataFrames",[10,1436,1437,1440],{},[37,1438,1439],{},"hypothesis.extra.pandas"," builds DataFrames column by column, which suits cleaning steps that take a frame. Here the property is for a function that removes rows where every cell is blank: no blank rows remain, and no non-blank row is lost or reordered:",[30,1442,1444],{"className":194,"code":1443,"language":196,"meta":35,"style":35},"import pandas as pd\nfrom hypothesis import given, settings, strategies as st\nfrom hypothesis.extra.pandas import column, data_frames\n\ncell = st.one_of(st.none(), st.just(\"\"), st.just(\"  \"), st.text(min_size=1, max_size=5))\n\ndef drop_blank_rows(df):\n    blank = df.apply(lambda col: col.isna() | col.astype(str).str.strip().eq(\"\")).all(axis=1)\n    return df[~blank].reset_index(drop=True)\n\n@settings(derandomize=True, max_examples=300)\n@given(data_frames([column(\"a\", elements=cell, dtype=object), column(\"b\", elements=cell, dtype=object)]))\ndef test_drop_blank_rows(df):\n    out = drop_blank_rows(df)\n    is_blank = lambda v: v is None or (isinstance(v, str) and not v.strip())\n    assert not any(all(is_blank(v) for v in row) for row in out.itertuples(index=False))\n    kept = [tuple(r) for r in df.itertuples(index=False) if not all(is_blank(v) for v in r)]\n    assert [tuple(r) for r in out.itertuples(index=False)] == kept\n",[37,1445,1446,1456,1470,1482,1486,1524,1528,1537,1578,1600,1604,1627,1677,1686,1695,1738,1783,1835],{"__ignoreMap":35},[40,1447,1448,1450,1452,1454],{"class":42,"line":43},[40,1449,204],{"class":203},[40,1451,526],{"class":207},[40,1453,225],{"class":203},[40,1455,531],{"class":207},[40,1457,1458,1460,1462,1464,1466,1468],{"class":42,"line":211},[40,1459,214],{"class":203},[40,1461,217],{"class":207},[40,1463,204],{"class":203},[40,1465,222],{"class":207},[40,1467,225],{"class":203},[40,1469,228],{"class":207},[40,1471,1472,1474,1477,1479],{"class":42,"line":231},[40,1473,214],{"class":203},[40,1475,1476],{"class":207}," hypothesis.extra.pandas ",[40,1478,204],{"class":203},[40,1480,1481],{"class":207}," column, data_frames\n",[40,1483,1484],{"class":42,"line":238},[40,1485,235],{"emptyLinePlaceholder":234},[40,1487,1488,1491,1493,1496,1499,1502,1505,1508,1510,1512,1514,1516,1518,1520,1522],{"class":42,"line":263},[40,1489,1490],{"class":207},"cell ",[40,1492,313],{"class":203},[40,1494,1495],{"class":207}," st.one_of(st.none(), st.just(",[40,1497,1498],{"class":50},"\"\"",[40,1500,1501],{"class":207},"), st.just(",[40,1503,1504],{"class":50},"\"  \"",[40,1506,1507],{"class":207},"), st.text(",[40,1509,691],{"class":309},[40,1511,313],{"class":203},[40,1513,696],{"class":251},[40,1515,286],{"class":207},[40,1517,680],{"class":309},[40,1519,313],{"class":203},[40,1521,705],{"class":251},[40,1523,708],{"class":207},[40,1525,1526],{"class":42,"line":295},[40,1527,235],{"emptyLinePlaceholder":234},[40,1529,1530,1532,1535],{"class":42,"line":300},[40,1531,241],{"class":203},[40,1533,1534],{"class":244}," drop_blank_rows",[40,1536,575],{"class":207},[40,1538,1539,1542,1544,1547,1550,1553,1556,1559,1561,1564,1566,1569,1572,1574,1576],{"class":42,"line":332},[40,1540,1541],{"class":207},"    blank ",[40,1543,313],{"class":203},[40,1545,1546],{"class":207}," df.apply(",[40,1548,1549],{"class":203},"lambda",[40,1551,1552],{"class":207}," col: col.isna() ",[40,1554,1555],{"class":203},"|",[40,1557,1558],{"class":207}," col.astype(",[40,1560,252],{"class":251},[40,1562,1563],{"class":207},").str.strip().eq(",[40,1565,1498],{"class":50},[40,1567,1568],{"class":207},")).all(",[40,1570,1571],{"class":309},"axis",[40,1573,313],{"class":203},[40,1575,696],{"class":251},[40,1577,329],{"class":207},[40,1579,1580,1582,1585,1588,1591,1594,1596,1598],{"class":42,"line":341},[40,1581,266],{"class":203},[40,1583,1584],{"class":207}," df[",[40,1586,1587],{"class":203},"~",[40,1589,1590],{"class":207},"blank].reset_index(",[40,1592,1593],{"class":309},"drop",[40,1595,313],{"class":203},[40,1597,316],{"class":251},[40,1599,329],{"class":207},[40,1601,1602],{"class":42,"line":352},[40,1603,235],{"emptyLinePlaceholder":234},[40,1605,1606,1608,1610,1612,1614,1616,1618,1620,1622,1625],{"class":42,"line":363},[40,1607,303],{"class":244},[40,1609,306],{"class":207},[40,1611,310],{"class":309},[40,1613,313],{"class":203},[40,1615,316],{"class":251},[40,1617,286],{"class":207},[40,1619,321],{"class":309},[40,1621,313],{"class":203},[40,1623,1624],{"class":251},"300",[40,1626,329],{"class":207},[40,1628,1629,1631,1634,1637,1639,1642,1644,1647,1649,1651,1654,1657,1660,1662,1664,1666,1668,1670,1672,1674],{"class":42,"line":381},[40,1630,335],{"class":244},[40,1632,1633],{"class":207},"(data_frames([column(",[40,1635,1636],{"class":50},"\"a\"",[40,1638,286],{"class":207},[40,1640,1641],{"class":309},"elements",[40,1643,313],{"class":203},[40,1645,1646],{"class":207},"cell, ",[40,1648,619],{"class":309},[40,1650,313],{"class":203},[40,1652,1653],{"class":251},"object",[40,1655,1656],{"class":207},"), column(",[40,1658,1659],{"class":50},"\"b\"",[40,1661,286],{"class":207},[40,1663,1641],{"class":309},[40,1665,313],{"class":203},[40,1667,1646],{"class":207},[40,1669,619],{"class":309},[40,1671,313],{"class":203},[40,1673,1653],{"class":251},[40,1675,1676],{"class":207},")]))\n",[40,1678,1679,1681,1684],{"class":42,"line":395},[40,1680,241],{"class":203},[40,1682,1683],{"class":244}," test_drop_blank_rows",[40,1685,575],{"class":207},[40,1687,1688,1690,1692],{"class":42,"line":400},[40,1689,355],{"class":207},[40,1691,313],{"class":203},[40,1693,1694],{"class":207}," drop_blank_rows(df)\n",[40,1696,1697,1700,1702,1705,1708,1711,1714,1717,1720,1723,1726,1728,1730,1733,1735],{"class":42,"line":423},[40,1698,1699],{"class":207},"    is_blank ",[40,1701,313],{"class":203},[40,1703,1704],{"class":203}," lambda",[40,1706,1707],{"class":207}," v: v ",[40,1709,1710],{"class":203},"is",[40,1712,1713],{"class":251}," None",[40,1715,1716],{"class":203}," or",[40,1718,1719],{"class":207}," (",[40,1721,1722],{"class":251},"isinstance",[40,1724,1725],{"class":207},"(v, ",[40,1727,252],{"class":251},[40,1729,1325],{"class":207},[40,1731,1732],{"class":203},"and",[40,1734,372],{"class":203},[40,1736,1737],{"class":207}," v.strip())\n",[40,1739,1740,1742,1744,1747,1749,1752,1755,1757,1760,1762,1765,1767,1770,1772,1775,1777,1779,1781],{"class":42,"line":430},[40,1741,366],{"class":203},[40,1743,372],{"class":203},[40,1745,1746],{"class":251}," any",[40,1748,306],{"class":207},[40,1750,1751],{"class":251},"all",[40,1753,1754],{"class":207},"(is_blank(v) ",[40,1756,1328],{"class":203},[40,1758,1759],{"class":207}," v ",[40,1761,1334],{"class":203},[40,1763,1764],{"class":207}," row) ",[40,1766,1328],{"class":203},[40,1768,1769],{"class":207}," row ",[40,1771,1334],{"class":203},[40,1773,1774],{"class":207}," out.itertuples(",[40,1776,593],{"class":309},[40,1778,313],{"class":203},[40,1780,598],{"class":251},[40,1782,708],{"class":207},[40,1784,1785,1788,1790,1793,1796,1799,1801,1804,1806,1809,1811,1813,1815,1817,1820,1822,1824,1826,1828,1830,1832],{"class":42,"line":440},[40,1786,1787],{"class":207},"    kept ",[40,1789,313],{"class":203},[40,1791,1792],{"class":207}," [",[40,1794,1795],{"class":251},"tuple",[40,1797,1798],{"class":207},"(r) ",[40,1800,1328],{"class":203},[40,1802,1803],{"class":207}," r ",[40,1805,1334],{"class":203},[40,1807,1808],{"class":207}," df.itertuples(",[40,1810,593],{"class":309},[40,1812,313],{"class":203},[40,1814,598],{"class":251},[40,1816,1325],{"class":207},[40,1818,1819],{"class":203},"if",[40,1821,372],{"class":203},[40,1823,1302],{"class":251},[40,1825,1754],{"class":207},[40,1827,1328],{"class":203},[40,1829,1759],{"class":207},[40,1831,1334],{"class":203},[40,1833,1834],{"class":207}," r)]\n",[40,1836,1837,1839,1841,1843,1845,1847,1849,1851,1853,1855,1857,1859,1862,1864],{"class":42,"line":1217},[40,1838,366],{"class":203},[40,1840,1792],{"class":207},[40,1842,1795],{"class":251},[40,1844,1798],{"class":207},[40,1846,1328],{"class":203},[40,1848,1803],{"class":207},[40,1850,1334],{"class":203},[40,1852,1774],{"class":207},[40,1854,593],{"class":309},[40,1856,313],{"class":203},[40,1858,598],{"class":251},[40,1860,1861],{"class":207},")] ",[40,1863,389],{"class":203},[40,1865,1866],{"class":207}," kept\n",[30,1868,1870],{"className":32,"code":1869,"language":34,"meta":35,"style":35},"python -m pytest -q -p no:cacheprovider test_frames.py 2>&1 | tail -1\n",[37,1871,1872],{"__ignoreMap":35},[40,1873,1874,1876,1878,1880,1882,1884,1886,1889,1891,1893,1895],{"class":42,"line":43},[40,1875,196],{"class":46},[40,1877,461],{"class":251},[40,1879,57],{"class":50},[40,1881,466],{"class":251},[40,1883,469],{"class":251},[40,1885,472],{"class":50},[40,1887,1888],{"class":50}," test_frames.py",[40,1890,478],{"class":203},[40,1892,481],{"class":203},[40,1894,484],{"class":46},[40,1896,487],{"class":251},[10,1898,1899,1900,1903,1904,1907],{},"The strategy for ",[37,1901,1902],{},"cell"," deliberately mixes ",[37,1905,1906],{},"None",", empty strings, whitespace-only strings and real text — the four states a \"blank\" cell can be in after reading a workbook. The test states both halves of correctness: nothing blank survives, and everything non-blank survives in order. A function that dropped too much would pass the first half and fail the second.",[25,1909,1911],{"id":1910},"step-5-running-property-tests-in-ci","Step 5 — Running property tests in CI",[10,1913,1914,1915,1918],{},"Hypothesis settings can live in a profile loaded by ",[37,1916,1917],{},"conftest.py",", so local runs stay fast and CI searches harder:",[30,1920,1922],{"className":194,"code":1921,"language":196,"meta":35,"style":35},"import os\nfrom hypothesis import HealthCheck, settings\n\nsettings.register_profile(\"dev\", max_examples=50)\nsettings.register_profile(\"ci\", max_examples=500, deadline=None,\n                          suppress_health_check=[HealthCheck.too_slow])\nsettings.load_profile(os.environ.get(\"HYPOTHESIS_PROFILE\", \"dev\"))\n",[37,1923,1924,1931,1942,1946,1965,1992,2001],{"__ignoreMap":35},[40,1925,1926,1928],{"class":42,"line":43},[40,1927,204],{"class":203},[40,1929,1930],{"class":207}," os\n",[40,1932,1933,1935,1937,1939],{"class":42,"line":211},[40,1934,214],{"class":203},[40,1936,217],{"class":207},[40,1938,204],{"class":203},[40,1940,1941],{"class":207}," HealthCheck, settings\n",[40,1943,1944],{"class":42,"line":231},[40,1945,235],{"emptyLinePlaceholder":234},[40,1947,1948,1951,1954,1956,1958,1960,1963],{"class":42,"line":238},[40,1949,1950],{"class":207},"settings.register_profile(",[40,1952,1953],{"class":50},"\"dev\"",[40,1955,286],{"class":207},[40,1957,321],{"class":309},[40,1959,313],{"class":203},[40,1961,1962],{"class":251},"50",[40,1964,329],{"class":207},[40,1966,1967,1969,1972,1974,1976,1978,1980,1982,1985,1987,1989],{"class":42,"line":263},[40,1968,1950],{"class":207},[40,1970,1971],{"class":50},"\"ci\"",[40,1973,286],{"class":207},[40,1975,321],{"class":309},[40,1977,313],{"class":203},[40,1979,326],{"class":251},[40,1981,286],{"class":207},[40,1983,1984],{"class":309},"deadline",[40,1986,313],{"class":203},[40,1988,1906],{"class":251},[40,1990,1991],{"class":207},",\n",[40,1993,1994,1997,1999],{"class":42,"line":295},[40,1995,1996],{"class":309},"                          suppress_health_check",[40,1998,313],{"class":203},[40,2000,1181],{"class":207},[40,2002,2003,2006,2009,2011,2013],{"class":42,"line":300},[40,2004,2005],{"class":207},"settings.load_profile(os.environ.get(",[40,2007,2008],{"class":50},"\"HYPOTHESIS_PROFILE\"",[40,2010,286],{"class":207},[40,2012,1953],{"class":50},[40,2014,708],{"class":207},[30,2016,2018],{"className":32,"code":2017,"language":34,"meta":35,"style":35},"HYPOTHESIS_PROFILE=ci python -m pytest -q -p no:cacheprovider test_clean.py test_frames.py 2>&1 | tail -1\n",[37,2019,2020],{"__ignoreMap":35},[40,2021,2022,2025,2027,2030,2033,2035,2037,2039,2041,2043,2045,2047,2049,2051,2053],{"class":42,"line":43},[40,2023,2024],{"class":207},"HYPOTHESIS_PROFILE",[40,2026,313],{"class":203},[40,2028,2029],{"class":50},"ci",[40,2031,2032],{"class":46}," python",[40,2034,461],{"class":251},[40,2036,57],{"class":50},[40,2038,466],{"class":251},[40,2040,469],{"class":251},[40,2042,472],{"class":50},[40,2044,475],{"class":50},[40,2046,1888],{"class":50},[40,2048,478],{"class":203},[40,2050,481],{"class":203},[40,2052,484],{"class":46},[40,2054,487],{"class":251},[79,2056,88,2061,88,2064,88,2067,88,2070,88,2074,88,2081,88,2087,88,2090,88,2093,88,2097,88,2102,88,2104,88,2108,88,2110,88,2112,88,2115,88,2119,88,2121,88,2124,88,2126,88,2128,88,2131,88,2135,88,2137,88,2143,88,2145,88,2149,88,2152,88,2156,88,2158,88,2161,88,2163],{"viewBox":2057,"role":82,"ariaLabelledBy":2058,"xmlns":86,"style":87},"4 0 752 208",[2059,2060],"hy-profiles-t","hy-profiles-d",[90,2062,2063],{"id":2059},"Two Hypothesis profiles for one test suite",[94,2065,2066],{"id":2060},"A comparison of the dev and ci profiles. Dev runs 50 examples per test with the default deadline for fast local feedback; ci runs 500 examples with no deadline and the too_slow health check suppressed.",[98,2068],{"x":100,"y":101,"width":102,"height":2069,"fill":104},"208",[98,2071],{"x":107,"y":108,"width":2072,"height":2073,"rx":101,"fill":104,"stroke":113,"style":138},"170.7","40.0",[98,2075],{"x":2076,"y":108,"width":2077,"height":2073,"rx":101,"fill":2078,"stroke":2079,"style":2080},"190.7","274.7","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.2px",[127,2082,2086],{"x":2083,"y":2084,"style":2085},"328.0","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","dev",[98,2088],{"x":2089,"y":108,"width":2077,"height":2073,"rx":101,"fill":2078,"stroke":2079,"style":2080},"465.3",[127,2091,2029],{"x":2092,"y":2084,"style":2085},"602.7",[98,2094],{"x":107,"y":2095,"width":2072,"height":2096,"rx":101,"fill":112,"stroke":113,"style":138},"56.0","34.0",[127,2098,2101],{"x":2099,"y":130,"style":2100},"32.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","examples per test",[98,2103],{"x":2076,"y":2095,"width":2077,"height":2096,"rx":101,"fill":104,"stroke":113,"style":138},[127,2105,1962],{"x":2083,"y":2106,"style":2107},"77.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[98,2109],{"x":2089,"y":2095,"width":2077,"height":2096,"rx":101,"fill":104,"stroke":113,"style":138},[127,2111,326],{"x":2092,"y":2106,"style":2107},[98,2113],{"x":107,"y":2114,"width":2072,"height":2096,"rx":101,"fill":112,"stroke":113,"style":138},"90.0",[127,2116,2118],{"x":2099,"y":2117,"style":2100},"112.0","deadline per example",[98,2120],{"x":2076,"y":2114,"width":2077,"height":2096,"rx":101,"fill":104,"stroke":113,"style":138},[127,2122,2123],{"x":2083,"y":141,"style":2107},"default",[98,2125],{"x":2089,"y":2114,"width":2077,"height":2096,"rx":101,"fill":104,"stroke":113,"style":138},[127,2127,123],{"x":2092,"y":141,"style":2107},[98,2129],{"x":107,"y":2130,"width":2072,"height":2096,"rx":101,"fill":112,"stroke":113,"style":138},"124.0",[127,2132,2134],{"x":2099,"y":2133,"style":2100},"146.0","too_slow health check",[98,2136],{"x":2076,"y":2130,"width":2077,"height":2096,"rx":101,"fill":104,"stroke":113,"style":138},[116,2138,2140],{"transform":2139},"translate(319.0,132.0) scale(0.750)",[120,2141],{"d":122,"fill":123,"stroke":156,"style":2142},"stroke-width:2.40px;stroke-linecap:round;stroke-linejoin:round",[98,2144],{"x":2089,"y":2130,"width":2077,"height":2096,"rx":101,"fill":104,"stroke":113,"style":138},[127,2146,2148],{"x":2092,"y":2147,"style":2107},"145.0","suppressed",[98,2150],{"x":107,"y":2151,"width":2072,"height":2096,"rx":101,"fill":112,"stroke":113,"style":138},"158.0",[127,2153,2155],{"x":2099,"y":2154,"style":2100},"180.0","selected by",[98,2157],{"x":2076,"y":2151,"width":2077,"height":2096,"rx":101,"fill":104,"stroke":113,"style":138},[127,2159,2123],{"x":2083,"y":2160,"style":2107},"179.0",[98,2162],{"x":2089,"y":2151,"width":2077,"height":2096,"rx":101,"fill":104,"stroke":113,"style":138},[127,2164,2165],{"x":2092,"y":2160,"style":2107},"HYPOTHESIS_PROFILE=ci",[10,2167,2168,2171,2172,2175,2176,2179,2180,2183],{},[37,2169,2170],{},"deadline=None"," stops Hypothesis failing tests that are slow because they write workbooks — slowness is not a bug here. When a CI run does find a failure, Hypothesis prints the minimal example and a ",[37,2173,2174],{},"@reproduce_failure"," decorator to paste into the test; add the example permanently with ",[37,2177,2178],{},"@example(...)"," so it stays covered. Hypothesis also keeps a local database of failing examples and retries them first on the next run, which is why the ",[37,2181,2182],{},"-p no:cacheprovider"," flag on these commands only affects pytest's own cache.",[25,2185,2187],{"id":2186},"common-pitfalls","Common pitfalls",[2189,2190,2191,2203,2209,2215,2224],"ul",{},[2192,2193,2194,2198,2199,2202],"li",{},[2195,2196,2197],"strong",{},"Properties that restate the implementation."," \"Output equals ",[37,2200,2201],{},"re.sub(...)"," of input\" tests nothing; state rules the output must satisfy.",[2192,2204,2205,2208],{},[2195,2206,2207],{},"Excluding the hard cases from the strategy."," Filtering out empty strings and control characters hides exactly the inputs real files contain; handle them in the code instead.",[2192,2210,2211,2214],{},[2195,2212,2213],{},"Exact float comparison across a file boundary."," Excel stores about 15 significant digits; compare with a tolerance or write as text.",[2192,2216,2217,2220,2221,2223],{},[2195,2218,2219],{},"Random, unreproducible CI runs."," Use ",[37,2222,76],{}," or a fixed seed profile for pipelines where flaky tests are costly.",[2192,2225,2226,2229,2230,2232],{},[2195,2227,2228],{},"Slow tests failing on deadlines."," Workbook I\u002FO is slow; set ",[37,2231,2170],{}," for those tests.",[25,2234,2236],{"id":2235},"conclusion","Conclusion",[10,2238,2239,2240,2242,2243,2245],{},"State properties instead of examples — idempotence, no leftover whitespace, blank rows removed without losing others — and let Hypothesis's text, float and DataFrame strategies search for counterexamples. The write-then-read round trip is the property that pays for itself fastest: it showed that empty strings vanish, control characters come back as ",[37,2241,805],{}," floats keep only about 15 significant digits, and numbers at the edge of the float range cannot be read back at all — each of which is a decision the cleaning code now makes explicitly. Keep the tests reproducible with ",[37,2244,76],{}," and tune their effort with profiles.",[25,2247,2249],{"id":2248},"frequently-asked-questions","Frequently asked questions",[10,2251,2252,2255],{},[2195,2253,2254],{},"What is property-based testing?","\nInstead of checking chosen inputs against expected outputs, you state properties that must hold for every input — such as \"cleaning twice equals cleaning once\" — and a library like Hypothesis generates many inputs to find a counterexample, which it then shrinks to the smallest failing case.",[10,2257,2258,2261,2262,2265],{},[2195,2259,2260],{},"How do I generate pandas DataFrames with Hypothesis?","\nUse hypothesis.extra.pandas: data_frames(",[40,2263,2264],{},"column(\"a\", elements=..., dtype=...), ...",") builds frames column by column from element strategies you choose, including None and empty strings.",[10,2267,2268,2271],{},[2195,2269,2270],{},"Why does an empty string disappear when I write it to Excel?","\nExcel does not distinguish an empty string from an empty cell, and empty cells are not stored in the file. A row whose cells are all empty strings therefore has nothing to read back.",[10,2273,2274,2277],{},[2195,2275,2276],{},"Why do my floats change after writing them to Excel?","\nExcel works with about 15 significant digits, and xlsxwriter writes numbers at that precision, so the last digits of a 17-digit float are rounded. Compare with a relative tolerance, or store values that must be exact as text.",[25,2279,2281],{"id":2280},"related","Related",[2189,2283,2284,2290,2297,2304],{},[2192,2285,2286,2287,2289],{},"Up one level: ",[19,2288,22],{"href":21}," — making report code dependable.",[2192,2291,2292,2296],{},[19,2293,2295],{"href":2294},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002F","Test Excel Output with pytest"," — example-based tests for workbooks.",[2192,2298,2299,2303],{},[19,2300,2302],{"href":2301},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-illegalcharactererror-when-writing-excel-with-openpyxl\u002F","Fix IllegalCharacterError When Writing Excel with openpyxl"," — control characters with the openpyxl engine.",[2192,2305,2306,2310],{},[19,2307,2309],{"href":2308},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-floating-point-noise-in-excel-output\u002F","Fix Floating Point Noise in Excel Output"," — the precision question from the other side.",[2312,2313,2314],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}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);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}",{"title":35,"searchDepth":211,"depth":211,"links":2316},[2317,2318,2319,2320,2321,2322,2323,2324,2325,2326],{"id":27,"depth":211,"text":28},{"id":183,"depth":211,"text":184},{"id":503,"depth":211,"text":504},{"id":981,"depth":211,"text":982},{"id":1433,"depth":211,"text":1434},{"id":1910,"depth":211,"text":1911},{"id":2186,"depth":211,"text":2187},{"id":2235,"depth":211,"text":2236},{"id":2248,"depth":211,"text":2249},{"id":2280,"depth":211,"text":2281},"2026-09-29","Let Hypothesis find the inputs your Excel code breaks on: property-based tests for a text-cleaning function, a round-trip property for writing and reading workbooks that uncovers vanishing empty strings, control characters returned as _x001F_ and floats cut to 15 significant digits, DataFrame strategies for cleaning steps, and settings for CI.","md",[2331,2333,2335,2337],{"q":2254,"a":2332},"Instead of checking chosen inputs against expected outputs, you state properties that must hold for every input — such as \"cleaning twice equals cleaning once\" — and a library like Hypothesis generates many inputs to find a counterexample, which it then shrinks to the smallest failing case.",{"q":2260,"a":2334},"Use hypothesis.extra.pandas: data_frames([column(\"a\", elements=..., dtype=...), ...]) builds frames column by column from element strategies you choose, including None and empty strings.",{"q":2270,"a":2336},"Excel does not distinguish an empty string from an empty cell, and empty cells are not stored in the file. A row whose cells are all empty strings therefore has nothing to read back.",{"q":2276,"a":2338},"Excel works with about 15 significant digits, and xlsxwriter writes numbers at that precision, so the last digits of a 17-digit float are rounded. Compare with a relative tolerance, or store values that must be exact as text.","test excel cleaning code with hypothesis",null,{"breadcrumb":2342},[2343,2346,2349],{"name":2344,"item":2345},"Home","\u002F",{"name":2347,"item":2348},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":22,"item":21},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-cleaning-code-with-hypothesis",{"title":2352,"description":2353},"Property-Based Testing of Excel Code with Hypothesis","Use Hypothesis to test Python Excel code: properties like idempotence and no double spaces, a to_excel\u002Fread_excel round-trip that finds empty strings, _x001F_ control characters and float precision limits, hypothesis.extra.pandas data_frames, derandomize and profiles.","test-excel-cleaning-code-with-hypothesis","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-cleaning-code-with-hypothesis\u002Findex",[2357,2358],"reliable-jobs","data-cleaning","how-to","2Qms1VDow7WJCjzQQSTnWfziic1Go0D5X0htMFenkpc",[2362,2366],{"title":2363,"path":2364,"stem":2365,"children":-1},"Store Excel Files in Git with Git LFS","\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fstore-excel-files-in-git-with-git-lfs","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fstore-excel-files-in-git-with-git-lfs\u002Findex",{"title":2367,"path":2368,"stem":2369,"children":-1},"Test Excel Code Against Several Library Versions with nox","\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-code-against-several-library-versions-with-nox","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-code-against-several-library-versions-with-nox\u002Findex",[2371,2373],{"path":2372,"title":2347},"\u002Fautomating-reporting-workflows",{"path":2374,"title":22},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts",[],[2377,2381,2384,2387,2390,2393],{"path":2378,"title":2379,"kind":2380},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation","Error Handling and Logging in Excel Automation","guide",{"path":2382,"title":2383,"kind":2359},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fdetect-a-scheduled-report-that-did-not-run","Detect a Scheduled Report That Did Not Run",{"path":2385,"title":2386,"kind":2359},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fignore-excel-lock-files-when-processing-a-folder","Ignore Excel Lock Files When Processing a Folder",{"path":2388,"title":2389,"kind":2359},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Flog-python-excel-script-output-to-a-file","Log Python Excel Script Output to a File",{"path":2391,"title":2392,"kind":2359},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fmake-an-excel-report-job-safe-to-rerun","Make an Excel Report Job Safe to Rerun",{"path":2394,"title":2395,"kind":2359},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-formula-injection-in-excel-exports","Prevent Formula Injection in Excel Exports from Python",1790972142891]