[{"data":1,"prerenderedAt":2407},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fhighlight-invalid-cells-in-excel-with-python":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fhighlight-invalid-cells-in-excel-with-python":2398},{"id":4,"title":5,"body":6,"dateModified":2368,"datePublished":2368,"description":2369,"extension":2370,"faq":2371,"meta":2380,"navigation":236,"path":2391,"seo":2392,"slug":2394,"stem":2395,"type":2396,"__hash__":2397},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fhighlight-invalid-cells-in-excel-with-python\u002Findex.md","Highlight Invalid Cells in Excel with Python",{"type":7,"value":8,"toc":2353},"minimark",[9,19,157,162,190,201,205,208,619,626,630,633,959,973,977,980,1259,1266,1387,1391,1394,1705,1708,1712,1715,1995,2010,2014,2081,2084,2088,2091,2145,2148,2152,2255,2259,2262,2266,2269,2273,2280,2286,2292,2301,2305,2308,2317,2320,2349],[10,11,12,13,18],"p",{},"A list of validation failures in a log file gets read by you. The same failures painted onto the cells that caused them get fixed by the person who typed them. This guide, part of ",[14,15,17],"a",{"href":16},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002F","Validating Excel Data with Python",", turns a list of issues into a workbook that explains itself: filled cells, comments carrying the reason, a legend, and an issues sheet.",[20,21,29,30,29,34,29,38,29,45,29,54,29,61,29,66,29,69,29,73,29,81,29,86,29,94,29,99,29,103,29,107,29,110,29,118,29,123,29,127,29,131,29,136,29,140,29,147,29,153],"svg",{"viewBox":22,"role":23,"ariaLabelledBy":24,"xmlns":27,"style":28},"0 0 740 250","img",[25,26],"hi-map-t","hi-map-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:740px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[31,32,33],"title",{"id":25},"From an issue record to a marked-up cell",[35,36,37],"desc",{"id":26},"An issue record holding row 4, column Quantity, value minus seven and the reason must be greater than zero is turned into a coordinate C4, then a red fill and a comment are applied to that cell and the same record is written to an issues sheet.",[39,40],"rect",{"x":41,"y":41,"width":42,"height":43,"fill":44},"0","740","250","#ffffff",[39,46],{"x":47,"y":48,"width":49,"height":50,"rx":51,"fill":52,"stroke":53},"20","40","230","110","12","#ebebfd","var(--line,#cdd5e6)",[55,56,60],"text",{"x":57,"y":58,"style":59},"135","66","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","issue record",[55,62,65],{"x":57,"y":63,"style":64},"90","font-size:11.5px;fill:var(--muted,#5b6780);text-anchor:middle","row: 4",[55,67,68],{"x":57,"y":50,"style":64},"column: Quantity",[55,70,72],{"x":57,"y":71,"style":64},"130","value: -7",[74,75],"line",{"x1":76,"y1":77,"x2":78,"y2":77,"stroke":79,"style":80},"254","95","290","var(--muted,#5b6780)","stroke-width:2px",[82,83],"polygon",{"points":84,"fill":85},"294,95 282,89 282,101","#5b6780",[39,87],{"x":88,"y":89,"width":90,"height":91,"rx":51,"fill":92,"stroke":93,"style":80},"296","56","150","78","#f0f4ff","var(--brand,#5b5cf0)",[55,95,98],{"x":96,"y":97,"style":59},"371","84","column → letter",[55,100,102],{"x":96,"y":50,"style":101},"font-size:13px;font-weight:700;fill:var(--text,#172033);text-anchor:middle;font-family:ui-monospace,Menlo,monospace","C4",[74,104],{"x1":105,"y1":77,"x2":106,"y2":77,"stroke":79,"style":80},"450","486",[82,108],{"points":109,"fill":85},"490,95 478,89 478,101",[39,111],{"x":112,"y":113,"width":114,"height":115,"rx":51,"fill":116,"stroke":117,"style":80},"492","30","228","70","#fce9e9","var(--danger,#dc2626)",[55,119,122],{"x":120,"y":89,"style":121},"606","font-size:12.5px;font-weight:700;fill:var(--danger,#dc2626);text-anchor:middle","red fill on the cell",[55,124,126],{"x":120,"y":125,"style":64},"80","visible at a glance",[39,128],{"x":112,"y":50,"width":114,"height":115,"rx":51,"fill":129,"stroke":130,"style":80},"#fdefd8","var(--gold,#b4740a)",[55,132,135],{"x":120,"y":133,"style":134},"136","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","comment with the reason",[55,137,139],{"x":120,"y":138,"style":64},"160","\"must be greater than zero\"",[39,141],{"x":88,"y":142,"width":143,"height":144,"rx":51,"fill":145,"stroke":146},"186","424","46","#d9f4f1","var(--teal,#0f9488)",[55,148,152],{"x":149,"y":150,"style":151},"508","206","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","and the same record on an Issues sheet",[55,154,156],{"x":149,"y":155,"style":64},"224","filterable, countable, easy to send on",[158,159,161],"h2",{"id":160},"prerequisites","Prerequisites",[163,164,169],"pre",{"className":165,"code":166,"language":167,"meta":168,"style":168},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[170,171,172],"code",{"__ignoreMap":168},[173,174,176,180,184,187],"span",{"class":74,"line":175},1,[173,177,179],{"class":178},"sMTad","pip",[173,181,183],{"class":182},"srMev"," install",[173,185,186],{"class":182}," pandas",[173,188,189],{"class":182}," openpyxl\n",[10,191,192,193,196,197,200],{},"The examples assume a list of issue dictionaries in the shape produced by the ",[14,194,195],{"href":16},"validation layer",": ",[170,198,199],{},"{\"row\": 4, \"column\": \"Quantity\", \"value\": -7, \"problem\": \"must be greater than zero\"}",".",[158,202,204],{"id":203},"write-the-data-then-mark-it-up","Write the data, then mark it up",[10,206,207],{},"Formatting must come after the data, because pandas replaces the whole sheet when it writes:",[163,209,213],{"className":210,"code":211,"language":212,"meta":168,"style":168},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\ndf = pd.DataFrame([\n    {\"Order_ID\": 2001, \"Region\": \"North\", \"Quantity\": 4, \"Unit_Price\": 19.99},\n    {\"Order_ID\": 2002, \"Region\": \"Souht\", \"Quantity\": 2, \"Unit_Price\": 49.50},\n    {\"Order_ID\": 2003, \"Region\": \"West\", \"Quantity\": -7, \"Unit_Price\": None},\n])\n\nissues = [\n    {\"row\": 3, \"column\": \"Region\", \"value\": \"Souht\", \"problem\": \"region is not on the approved list\"},\n    {\"row\": 4, \"column\": \"Quantity\", \"value\": -7, \"problem\": \"quantity must be greater than zero\"},\n    {\"row\": 4, \"column\": \"Unit_Price\", \"value\": None, \"problem\": \"unit price is missing\"},\n]\n\nwith pd.ExcelWriter(\"marked.xlsx\", engine=\"openpyxl\") as writer:\n    df.to_excel(writer, sheet_name=\"Orders\", index=False)\n    pd.DataFrame(issues).to_excel(writer, sheet_name=\"Issues\", index=False)\n","python",[170,214,215,231,238,250,299,340,384,390,395,406,449,489,527,533,538,569,596],{"__ignoreMap":168},[173,216,217,221,225,228],{"class":74,"line":175},[173,218,220],{"class":219},"s-kum","import",[173,222,224],{"class":223},"skGVy"," pandas ",[173,226,227],{"class":219},"as",[173,229,230],{"class":223}," pd\n",[173,232,234],{"class":74,"line":233},2,[173,235,237],{"emptyLinePlaceholder":236},true,"\n",[173,239,241,244,247],{"class":74,"line":240},3,[173,242,243],{"class":223},"df ",[173,245,246],{"class":219},"=",[173,248,249],{"class":223}," pd.DataFrame([\n",[173,251,253,256,259,261,265,268,271,273,276,278,281,283,286,288,291,293,296],{"class":74,"line":252},4,[173,254,255],{"class":223},"    {",[173,257,258],{"class":182},"\"Order_ID\"",[173,260,196],{"class":223},[173,262,264],{"class":263},"sP0c6","2001",[173,266,267],{"class":223},", ",[173,269,270],{"class":182},"\"Region\"",[173,272,196],{"class":223},[173,274,275],{"class":182},"\"North\"",[173,277,267],{"class":223},[173,279,280],{"class":182},"\"Quantity\"",[173,282,196],{"class":223},[173,284,285],{"class":263},"4",[173,287,267],{"class":223},[173,289,290],{"class":182},"\"Unit_Price\"",[173,292,196],{"class":223},[173,294,295],{"class":263},"19.99",[173,297,298],{"class":223},"},\n",[173,300,302,304,306,308,311,313,315,317,320,322,324,326,329,331,333,335,338],{"class":74,"line":301},5,[173,303,255],{"class":223},[173,305,258],{"class":182},[173,307,196],{"class":223},[173,309,310],{"class":263},"2002",[173,312,267],{"class":223},[173,314,270],{"class":182},[173,316,196],{"class":223},[173,318,319],{"class":182},"\"Souht\"",[173,321,267],{"class":223},[173,323,280],{"class":182},[173,325,196],{"class":223},[173,327,328],{"class":263},"2",[173,330,267],{"class":223},[173,332,290],{"class":182},[173,334,196],{"class":223},[173,336,337],{"class":263},"49.50",[173,339,298],{"class":223},[173,341,343,345,347,349,352,354,356,358,361,363,365,367,370,373,375,377,379,382],{"class":74,"line":342},6,[173,344,255],{"class":223},[173,346,258],{"class":182},[173,348,196],{"class":223},[173,350,351],{"class":263},"2003",[173,353,267],{"class":223},[173,355,270],{"class":182},[173,357,196],{"class":223},[173,359,360],{"class":182},"\"West\"",[173,362,267],{"class":223},[173,364,280],{"class":182},[173,366,196],{"class":223},[173,368,369],{"class":219},"-",[173,371,372],{"class":263},"7",[173,374,267],{"class":223},[173,376,290],{"class":182},[173,378,196],{"class":223},[173,380,381],{"class":263},"None",[173,383,298],{"class":223},[173,385,387],{"class":74,"line":386},7,[173,388,389],{"class":223},"])\n",[173,391,393],{"class":74,"line":392},8,[173,394,237],{"emptyLinePlaceholder":236},[173,396,398,401,403],{"class":74,"line":397},9,[173,399,400],{"class":223},"issues ",[173,402,246],{"class":219},[173,404,405],{"class":223}," [\n",[173,407,409,411,414,416,419,421,424,426,428,430,433,435,437,439,442,444,447],{"class":74,"line":408},10,[173,410,255],{"class":223},[173,412,413],{"class":182},"\"row\"",[173,415,196],{"class":223},[173,417,418],{"class":263},"3",[173,420,267],{"class":223},[173,422,423],{"class":182},"\"column\"",[173,425,196],{"class":223},[173,427,270],{"class":182},[173,429,267],{"class":223},[173,431,432],{"class":182},"\"value\"",[173,434,196],{"class":223},[173,436,319],{"class":182},[173,438,267],{"class":223},[173,440,441],{"class":182},"\"problem\"",[173,443,196],{"class":223},[173,445,446],{"class":182},"\"region is not on the approved list\"",[173,448,298],{"class":223},[173,450,452,454,456,458,460,462,464,466,468,470,472,474,476,478,480,482,484,487],{"class":74,"line":451},11,[173,453,255],{"class":223},[173,455,413],{"class":182},[173,457,196],{"class":223},[173,459,285],{"class":263},[173,461,267],{"class":223},[173,463,423],{"class":182},[173,465,196],{"class":223},[173,467,280],{"class":182},[173,469,267],{"class":223},[173,471,432],{"class":182},[173,473,196],{"class":223},[173,475,369],{"class":219},[173,477,372],{"class":263},[173,479,267],{"class":223},[173,481,441],{"class":182},[173,483,196],{"class":223},[173,485,486],{"class":182},"\"quantity must be greater than zero\"",[173,488,298],{"class":223},[173,490,492,494,496,498,500,502,504,506,508,510,512,514,516,518,520,522,525],{"class":74,"line":491},12,[173,493,255],{"class":223},[173,495,413],{"class":182},[173,497,196],{"class":223},[173,499,285],{"class":263},[173,501,267],{"class":223},[173,503,423],{"class":182},[173,505,196],{"class":223},[173,507,290],{"class":182},[173,509,267],{"class":223},[173,511,432],{"class":182},[173,513,196],{"class":223},[173,515,381],{"class":263},[173,517,267],{"class":223},[173,519,441],{"class":182},[173,521,196],{"class":223},[173,523,524],{"class":182},"\"unit price is missing\"",[173,526,298],{"class":223},[173,528,530],{"class":74,"line":529},13,[173,531,532],{"class":223},"]\n",[173,534,536],{"class":74,"line":535},14,[173,537,237],{"emptyLinePlaceholder":236},[173,539,541,544,547,550,552,556,558,561,564,566],{"class":74,"line":540},15,[173,542,543],{"class":219},"with",[173,545,546],{"class":223}," pd.ExcelWriter(",[173,548,549],{"class":182},"\"marked.xlsx\"",[173,551,267],{"class":223},[173,553,555],{"class":554},"sa561","engine",[173,557,246],{"class":219},[173,559,560],{"class":182},"\"openpyxl\"",[173,562,563],{"class":223},") ",[173,565,227],{"class":219},[173,567,568],{"class":223}," writer:\n",[173,570,572,575,578,580,583,585,588,590,593],{"class":74,"line":571},16,[173,573,574],{"class":223},"    df.to_excel(writer, ",[173,576,577],{"class":554},"sheet_name",[173,579,246],{"class":219},[173,581,582],{"class":182},"\"Orders\"",[173,584,267],{"class":223},[173,586,587],{"class":554},"index",[173,589,246],{"class":219},[173,591,592],{"class":263},"False",[173,594,595],{"class":223},")\n",[173,597,599,602,604,606,609,611,613,615,617],{"class":74,"line":598},17,[173,600,601],{"class":223},"    pd.DataFrame(issues).to_excel(writer, ",[173,603,577],{"class":554},[173,605,246],{"class":219},[173,607,608],{"class":182},"\"Issues\"",[173,610,267],{"class":223},[173,612,587],{"class":554},[173,614,246],{"class":219},[173,616,592],{"class":263},[173,618,595],{"class":223},[10,620,621,622,625],{},"The ",[170,623,624],{},"row"," values are spreadsheet rows: row 1 is the header, so the first data row is 2. Getting this off by one is the single most common bug in this whole exercise, and it is invisible until someone points at a perfectly good cell and asks what is wrong with it.",[158,627,629],{"id":628},"fill-the-offending-cells","Fill the offending cells",[10,631,632],{},"Map each issue's column name to a column letter, then apply a fill:",[163,634,636],{"className":210,"code":635,"language":212,"meta":168,"style":168},"from openpyxl import load_workbook\nfrom openpyxl.styles import PatternFill, Font\nfrom openpyxl.utils import get_column_letter\n\nBAD = PatternFill(start_color=\"FFC7CE\", end_color=\"FFC7CE\", fill_type=\"solid\")\nBAD_FONT = Font(color=\"9C0006\")\n\nwb = load_workbook(\"marked.xlsx\")\nws = wb[\"Orders\"]\n\nheaders = {cell.value: cell.column for cell in ws[1]}\n\nfor issue in issues:\n    col = headers.get(issue[\"column\"])\n    if col is None:\n        continue                      # column not on this sheet — skip rather than crash\n    cell = ws.cell(row=issue[\"row\"], column=col)\n    cell.fill = BAD\n    cell.font = BAD_FONT\n\nwb.save(\"marked.xlsx\")\nprint(f\"highlighted {len(issues)} cell(s)\")\n",[170,637,638,651,663,675,679,719,739,743,757,771,775,803,807,819,833,850,859,889,900,911,916,926],{"__ignoreMap":168},[173,639,640,643,646,648],{"class":74,"line":175},[173,641,642],{"class":219},"from",[173,644,645],{"class":223}," openpyxl ",[173,647,220],{"class":219},[173,649,650],{"class":223}," load_workbook\n",[173,652,653,655,658,660],{"class":74,"line":233},[173,654,642],{"class":219},[173,656,657],{"class":223}," openpyxl.styles ",[173,659,220],{"class":219},[173,661,662],{"class":223}," PatternFill, Font\n",[173,664,665,667,670,672],{"class":74,"line":240},[173,666,642],{"class":219},[173,668,669],{"class":223}," openpyxl.utils ",[173,671,220],{"class":219},[173,673,674],{"class":223}," get_column_letter\n",[173,676,677],{"class":74,"line":252},[173,678,237],{"emptyLinePlaceholder":236},[173,680,681,684,687,690,693,695,698,700,703,705,707,709,712,714,717],{"class":74,"line":301},[173,682,683],{"class":263},"BAD",[173,685,686],{"class":219}," =",[173,688,689],{"class":223}," PatternFill(",[173,691,692],{"class":554},"start_color",[173,694,246],{"class":219},[173,696,697],{"class":182},"\"FFC7CE\"",[173,699,267],{"class":223},[173,701,702],{"class":554},"end_color",[173,704,246],{"class":219},[173,706,697],{"class":182},[173,708,267],{"class":223},[173,710,711],{"class":554},"fill_type",[173,713,246],{"class":219},[173,715,716],{"class":182},"\"solid\"",[173,718,595],{"class":223},[173,720,721,724,726,729,732,734,737],{"class":74,"line":342},[173,722,723],{"class":263},"BAD_FONT",[173,725,686],{"class":219},[173,727,728],{"class":223}," Font(",[173,730,731],{"class":554},"color",[173,733,246],{"class":219},[173,735,736],{"class":182},"\"9C0006\"",[173,738,595],{"class":223},[173,740,741],{"class":74,"line":386},[173,742,237],{"emptyLinePlaceholder":236},[173,744,745,748,750,753,755],{"class":74,"line":392},[173,746,747],{"class":223},"wb ",[173,749,246],{"class":219},[173,751,752],{"class":223}," load_workbook(",[173,754,549],{"class":182},[173,756,595],{"class":223},[173,758,759,762,764,767,769],{"class":74,"line":397},[173,760,761],{"class":223},"ws ",[173,763,246],{"class":219},[173,765,766],{"class":223}," wb[",[173,768,582],{"class":182},[173,770,532],{"class":223},[173,772,773],{"class":74,"line":408},[173,774,237],{"emptyLinePlaceholder":236},[173,776,777,780,782,785,788,791,794,797,800],{"class":74,"line":451},[173,778,779],{"class":223},"headers ",[173,781,246],{"class":219},[173,783,784],{"class":223}," {cell.value: cell.column ",[173,786,787],{"class":219},"for",[173,789,790],{"class":223}," cell ",[173,792,793],{"class":219},"in",[173,795,796],{"class":223}," ws[",[173,798,799],{"class":263},"1",[173,801,802],{"class":223},"]}\n",[173,804,805],{"class":74,"line":491},[173,806,237],{"emptyLinePlaceholder":236},[173,808,809,811,814,816],{"class":74,"line":529},[173,810,787],{"class":219},[173,812,813],{"class":223}," issue ",[173,815,793],{"class":219},[173,817,818],{"class":223}," issues:\n",[173,820,821,824,826,829,831],{"class":74,"line":535},[173,822,823],{"class":223},"    col ",[173,825,246],{"class":219},[173,827,828],{"class":223}," headers.get(issue[",[173,830,423],{"class":182},[173,832,389],{"class":223},[173,834,835,838,841,844,847],{"class":74,"line":540},[173,836,837],{"class":219},"    if",[173,839,840],{"class":223}," col ",[173,842,843],{"class":219},"is",[173,845,846],{"class":263}," None",[173,848,849],{"class":223},":\n",[173,851,852,855],{"class":74,"line":571},[173,853,854],{"class":219},"        continue",[173,856,858],{"class":857},"s-wDw","                      # column not on this sheet — skip rather than crash\n",[173,860,861,864,866,869,871,873,876,878,881,884,886],{"class":74,"line":598},[173,862,863],{"class":223},"    cell ",[173,865,246],{"class":219},[173,867,868],{"class":223}," ws.cell(",[173,870,624],{"class":554},[173,872,246],{"class":219},[173,874,875],{"class":223},"issue[",[173,877,413],{"class":182},[173,879,880],{"class":223},"], ",[173,882,883],{"class":554},"column",[173,885,246],{"class":219},[173,887,888],{"class":223},"col)\n",[173,890,892,895,897],{"class":74,"line":891},18,[173,893,894],{"class":223},"    cell.fill ",[173,896,246],{"class":219},[173,898,899],{"class":263}," BAD\n",[173,901,903,906,908],{"class":74,"line":902},19,[173,904,905],{"class":223},"    cell.font ",[173,907,246],{"class":219},[173,909,910],{"class":263}," BAD_FONT\n",[173,912,914],{"class":74,"line":913},20,[173,915,237],{"emptyLinePlaceholder":236},[173,917,919,922,924],{"class":74,"line":918},21,[173,920,921],{"class":223},"wb.save(",[173,923,549],{"class":182},[173,925,595],{"class":223},[173,927,929,932,935,938,941,945,948,951,954,957],{"class":74,"line":928},22,[173,930,931],{"class":263},"print",[173,933,934],{"class":223},"(",[173,936,937],{"class":219},"f",[173,939,940],{"class":182},"\"highlighted ",[173,942,944],{"class":943},"sSjpA","{",[173,946,947],{"class":263},"len",[173,949,950],{"class":223},"(issues)",[173,952,953],{"class":943},"}",[173,955,956],{"class":182}," cell(s)\"",[173,958,595],{"class":223},[10,960,961,964,965,968,969,972],{},[170,962,963],{},"FFC7CE"," with ",[170,966,967],{},"9C0006"," text is the colour pair Excel itself uses for its \"Bad\" cell style, so the result looks native rather than like something a script did. Building ",[170,970,971],{},"headers"," from the sheet's own first row means the mapping is correct even when the column order differs from your DataFrame's.",[158,974,976],{"id":975},"attach-the-reason-to-the-cell","Attach the reason to the cell",[10,978,979],{},"A red cell says something is wrong; a comment says what:",[163,981,983],{"className":210,"code":982,"language":212,"meta":168,"style":168},"from openpyxl import load_workbook\nfrom openpyxl.comments import Comment\n\nwb = load_workbook(\"marked.xlsx\")\nws = wb[\"Orders\"]\nheaders = {cell.value: cell.column for cell in ws[1]}\n\nby_cell = {}\nfor issue in issues:\n    col = headers.get(issue[\"column\"])\n    if col:\n        by_cell.setdefault((issue[\"row\"], col), []).append(issue[\"problem\"])\n\nfor (row, col), problems in by_cell.items():\n    cell = ws.cell(row=row, column=col)\n    text = \"\\n\".join(f\"• {p}\" for p in problems)\n    comment = Comment(text, \"validation\")\n    comment.width, comment.height = 260, 26 + 18 * len(problems)\n    cell.comment = comment\n\nwb.save(\"marked.xlsx\")\n",[170,984,985,995,1007,1011,1023,1035,1055,1059,1069,1079,1091,1098,1112,1116,1128,1149,1192,1207,1237,1247,1251],{"__ignoreMap":168},[173,986,987,989,991,993],{"class":74,"line":175},[173,988,642],{"class":219},[173,990,645],{"class":223},[173,992,220],{"class":219},[173,994,650],{"class":223},[173,996,997,999,1002,1004],{"class":74,"line":233},[173,998,642],{"class":219},[173,1000,1001],{"class":223}," openpyxl.comments ",[173,1003,220],{"class":219},[173,1005,1006],{"class":223}," Comment\n",[173,1008,1009],{"class":74,"line":240},[173,1010,237],{"emptyLinePlaceholder":236},[173,1012,1013,1015,1017,1019,1021],{"class":74,"line":252},[173,1014,747],{"class":223},[173,1016,246],{"class":219},[173,1018,752],{"class":223},[173,1020,549],{"class":182},[173,1022,595],{"class":223},[173,1024,1025,1027,1029,1031,1033],{"class":74,"line":301},[173,1026,761],{"class":223},[173,1028,246],{"class":219},[173,1030,766],{"class":223},[173,1032,582],{"class":182},[173,1034,532],{"class":223},[173,1036,1037,1039,1041,1043,1045,1047,1049,1051,1053],{"class":74,"line":342},[173,1038,779],{"class":223},[173,1040,246],{"class":219},[173,1042,784],{"class":223},[173,1044,787],{"class":219},[173,1046,790],{"class":223},[173,1048,793],{"class":219},[173,1050,796],{"class":223},[173,1052,799],{"class":263},[173,1054,802],{"class":223},[173,1056,1057],{"class":74,"line":386},[173,1058,237],{"emptyLinePlaceholder":236},[173,1060,1061,1064,1066],{"class":74,"line":392},[173,1062,1063],{"class":223},"by_cell ",[173,1065,246],{"class":219},[173,1067,1068],{"class":223}," {}\n",[173,1070,1071,1073,1075,1077],{"class":74,"line":397},[173,1072,787],{"class":219},[173,1074,813],{"class":223},[173,1076,793],{"class":219},[173,1078,818],{"class":223},[173,1080,1081,1083,1085,1087,1089],{"class":74,"line":408},[173,1082,823],{"class":223},[173,1084,246],{"class":219},[173,1086,828],{"class":223},[173,1088,423],{"class":182},[173,1090,389],{"class":223},[173,1092,1093,1095],{"class":74,"line":451},[173,1094,837],{"class":219},[173,1096,1097],{"class":223}," col:\n",[173,1099,1100,1103,1105,1108,1110],{"class":74,"line":491},[173,1101,1102],{"class":223},"        by_cell.setdefault((issue[",[173,1104,413],{"class":182},[173,1106,1107],{"class":223},"], col), []).append(issue[",[173,1109,441],{"class":182},[173,1111,389],{"class":223},[173,1113,1114],{"class":74,"line":529},[173,1115,237],{"emptyLinePlaceholder":236},[173,1117,1118,1120,1123,1125],{"class":74,"line":535},[173,1119,787],{"class":219},[173,1121,1122],{"class":223}," (row, col), problems ",[173,1124,793],{"class":219},[173,1126,1127],{"class":223}," by_cell.items():\n",[173,1129,1130,1132,1134,1136,1138,1140,1143,1145,1147],{"class":74,"line":540},[173,1131,863],{"class":223},[173,1133,246],{"class":219},[173,1135,868],{"class":223},[173,1137,624],{"class":554},[173,1139,246],{"class":219},[173,1141,1142],{"class":223},"row, ",[173,1144,883],{"class":554},[173,1146,246],{"class":219},[173,1148,888],{"class":223},[173,1150,1151,1154,1156,1159,1162,1165,1168,1170,1173,1175,1177,1179,1181,1184,1187,1189],{"class":74,"line":571},[173,1152,1153],{"class":223},"    text ",[173,1155,246],{"class":219},[173,1157,1158],{"class":182}," \"",[173,1160,1161],{"class":943},"\\n",[173,1163,1164],{"class":182},"\"",[173,1166,1167],{"class":223},".join(",[173,1169,937],{"class":219},[173,1171,1172],{"class":182},"\"• ",[173,1174,944],{"class":943},[173,1176,10],{"class":223},[173,1178,953],{"class":943},[173,1180,1164],{"class":182},[173,1182,1183],{"class":219}," for",[173,1185,1186],{"class":223}," p ",[173,1188,793],{"class":219},[173,1190,1191],{"class":223}," problems)\n",[173,1193,1194,1197,1199,1202,1205],{"class":74,"line":598},[173,1195,1196],{"class":223},"    comment ",[173,1198,246],{"class":219},[173,1200,1201],{"class":223}," Comment(text, ",[173,1203,1204],{"class":182},"\"validation\"",[173,1206,595],{"class":223},[173,1208,1209,1212,1214,1217,1219,1222,1225,1228,1231,1234],{"class":74,"line":891},[173,1210,1211],{"class":223},"    comment.width, comment.height ",[173,1213,246],{"class":219},[173,1215,1216],{"class":263}," 260",[173,1218,267],{"class":223},[173,1220,1221],{"class":263},"26",[173,1223,1224],{"class":219}," +",[173,1226,1227],{"class":263}," 18",[173,1229,1230],{"class":219}," *",[173,1232,1233],{"class":263}," len",[173,1235,1236],{"class":223},"(problems)\n",[173,1238,1239,1242,1244],{"class":74,"line":902},[173,1240,1241],{"class":223},"    cell.comment ",[173,1243,246],{"class":219},[173,1245,1246],{"class":223}," comment\n",[173,1248,1249],{"class":74,"line":913},[173,1250,237],{"emptyLinePlaceholder":236},[173,1252,1253,1255,1257],{"class":74,"line":918},[173,1254,921],{"class":223},[173,1256,549],{"class":182},[173,1258,595],{"class":223},[10,1260,1261,1262,1265],{},"Grouping by coordinate first matters because one cell can break several rules at once, and assigning a second ",[170,1263,1264],{},"Comment"," to the same cell replaces the first. Sizing the comment box to the number of lines stops the text being clipped, which is otherwise the difference between a reader seeing the reason and seeing a truncated fragment.",[20,1267,29,1272,29,1275,29,1278,29,1281,29,1286,29,1292,29,1295,29,1299,29,1303,29,1306,29,1309,29,1312,29,1314,29,1316,29,1319,29,1322,29,1327,29,1331,29,1335,29,1337,29,1339,29,1342,29,1345,29,1348,29,1351,29,1354,29,1357,29,1361,29,1364,29,1367,29,1372,29,1376,29,1382],{"viewBox":1268,"role":23,"ariaLabelledBy":1269,"xmlns":27,"style":28},"0 0 740 246",[1270,1271],"hi-sheet-t","hi-sheet-d",[31,1273,1274],{"id":1270},"A marked-up sheet as the reader sees it",[35,1276,1277],{"id":1271},"A three-row orders table where the Region cell on row 3 and the Quantity and Unit Price cells on row 4 are filled red with dark red text, each carrying a comment marker, and a legend below the table explains the colour.",[39,1279],{"x":41,"y":41,"width":42,"height":1280,"fill":44},"246",[39,1282],{"x":113,"y":1221,"width":1283,"height":1284,"fill":1285},"672","32","#5b5cf0",[55,1287,1291],{"x":1288,"y":1289,"style":1290},"60","47","font-size:12px;font-weight:700;fill:#ffffff","Order_ID",[55,1293,1294],{"x":49,"y":1289,"style":1290},"Region",[55,1296,1298],{"x":1297,"y":1289,"style":1290},"420","Quantity",[55,1300,1302],{"x":1301,"y":1289,"style":1290},"580","Unit_Price",[39,1304],{"x":113,"y":1305,"width":1283,"height":113,"fill":44,"stroke":53},"58",[55,1307,264],{"x":1288,"y":91,"style":1308},"font-size:12px;fill:var(--text,#172033)",[55,1310,1311],{"x":49,"y":91,"style":1308},"North",[55,1313,285],{"x":1297,"y":91,"style":1308},[55,1315,295],{"x":1301,"y":91,"style":1308},[39,1317],{"x":113,"y":1318,"width":1283,"height":113,"fill":44,"stroke":53},"88",[55,1320,310],{"x":1288,"y":1321,"style":1308},"108",[39,1323],{"x":1324,"y":1318,"width":1325,"height":113,"fill":1326},"200","180","#FFC7CE",[55,1328,1330],{"x":49,"y":1321,"style":1329},"font-size:12px;font-weight:700;fill:#9C0006","Souht",[82,1332],{"points":1333,"fill":1334},"380,88 380,98 370,88","#9C0006",[55,1336,328],{"x":1297,"y":1321,"style":1308},[55,1338,337],{"x":1301,"y":1321,"style":1308},[39,1340],{"x":113,"y":1341,"width":1283,"height":113,"fill":44,"stroke":53},"118",[55,1343,351],{"x":1288,"y":1344,"style":1308},"138",[55,1346,1347],{"x":49,"y":1344,"style":1308},"West",[39,1349],{"x":1350,"y":1341,"width":90,"height":113,"fill":1326},"400",[55,1352,1353],{"x":1297,"y":1344,"style":1329},"-7",[82,1355],{"points":1356,"fill":1334},"550,118 550,128 540,118",[39,1358],{"x":1359,"y":1341,"width":1360,"height":113,"fill":1326},"560","142",[55,1362,1363],{"x":1301,"y":1344,"style":1329},"(blank)",[82,1365],{"points":1366,"fill":1334},"702,118 702,128 692,118",[39,1368],{"x":113,"y":1369,"width":1370,"height":144,"rx":1371,"fill":92,"stroke":53},"176","300","10",[39,1373],{"x":144,"y":1374,"width":1375,"height":1375,"fill":1326},"190","18",[55,1377,1381],{"x":1378,"y":1379,"style":1380},"76","205","font-size:11.5px;fill:var(--text,#172033)","failed validation — see the comment",[55,1383,1386],{"x":1384,"y":1379,"style":1385},"380","font-size:11.5px;fill:var(--muted,#5b6780)","Legend and comments make the file self-explaining.",[158,1388,1390],{"id":1389},"add-a-legend-and-an-active-sheet","Add a legend and an active sheet",[10,1392,1393],{},"Readers should not have to guess what the colour means, and the workbook should open on the sheet that explains itself:",[163,1395,1397],{"className":210,"code":1396,"language":212,"meta":168,"style":168},"from openpyxl import load_workbook\nfrom openpyxl.styles import PatternFill, Font\n\nwb = load_workbook(\"marked.xlsx\")\nws = wb[\"Orders\"]\n\nlegend_row = ws.max_row + 2\nws.cell(row=legend_row, column=1).fill = PatternFill(\"solid\", start_color=\"FFC7CE\")\nws.cell(row=legend_row, column=2, value=\"Failed validation — hover the cell for the reason\")\nws.cell(row=legend_row, column=2).font = Font(italic=True, color=\"595959\")\n\nsummary_row = legend_row + 1\nws.cell(row=summary_row, column=2,\n        value=f\"{len(issues)} issue(s) found in {len({i['row'] for i in issues})} row(s)\")\n\nws.freeze_panes = \"A2\"\nwb.active = wb.index(wb[\"Issues\"])       # open on the issues list\nwb.save(\"marked.xlsx\")\n",[170,1398,1399,1409,1419,1423,1435,1447,1451,1467,1504,1532,1574,1578,1593,1613,1665,1669,1679,1697],{"__ignoreMap":168},[173,1400,1401,1403,1405,1407],{"class":74,"line":175},[173,1402,642],{"class":219},[173,1404,645],{"class":223},[173,1406,220],{"class":219},[173,1408,650],{"class":223},[173,1410,1411,1413,1415,1417],{"class":74,"line":233},[173,1412,642],{"class":219},[173,1414,657],{"class":223},[173,1416,220],{"class":219},[173,1418,662],{"class":223},[173,1420,1421],{"class":74,"line":240},[173,1422,237],{"emptyLinePlaceholder":236},[173,1424,1425,1427,1429,1431,1433],{"class":74,"line":252},[173,1426,747],{"class":223},[173,1428,246],{"class":219},[173,1430,752],{"class":223},[173,1432,549],{"class":182},[173,1434,595],{"class":223},[173,1436,1437,1439,1441,1443,1445],{"class":74,"line":301},[173,1438,761],{"class":223},[173,1440,246],{"class":219},[173,1442,766],{"class":223},[173,1444,582],{"class":182},[173,1446,532],{"class":223},[173,1448,1449],{"class":74,"line":342},[173,1450,237],{"emptyLinePlaceholder":236},[173,1452,1453,1456,1458,1461,1464],{"class":74,"line":386},[173,1454,1455],{"class":223},"legend_row ",[173,1457,246],{"class":219},[173,1459,1460],{"class":223}," ws.max_row ",[173,1462,1463],{"class":219},"+",[173,1465,1466],{"class":263}," 2\n",[173,1468,1469,1472,1474,1476,1479,1481,1483,1485,1488,1490,1492,1494,1496,1498,1500,1502],{"class":74,"line":392},[173,1470,1471],{"class":223},"ws.cell(",[173,1473,624],{"class":554},[173,1475,246],{"class":219},[173,1477,1478],{"class":223},"legend_row, ",[173,1480,883],{"class":554},[173,1482,246],{"class":219},[173,1484,799],{"class":263},[173,1486,1487],{"class":223},").fill ",[173,1489,246],{"class":219},[173,1491,689],{"class":223},[173,1493,716],{"class":182},[173,1495,267],{"class":223},[173,1497,692],{"class":554},[173,1499,246],{"class":219},[173,1501,697],{"class":182},[173,1503,595],{"class":223},[173,1505,1506,1508,1510,1512,1514,1516,1518,1520,1522,1525,1527,1530],{"class":74,"line":397},[173,1507,1471],{"class":223},[173,1509,624],{"class":554},[173,1511,246],{"class":219},[173,1513,1478],{"class":223},[173,1515,883],{"class":554},[173,1517,246],{"class":219},[173,1519,328],{"class":263},[173,1521,267],{"class":223},[173,1523,1524],{"class":554},"value",[173,1526,246],{"class":219},[173,1528,1529],{"class":182},"\"Failed validation — hover the cell for the reason\"",[173,1531,595],{"class":223},[173,1533,1534,1536,1538,1540,1542,1544,1546,1548,1551,1553,1555,1558,1560,1563,1565,1567,1569,1572],{"class":74,"line":408},[173,1535,1471],{"class":223},[173,1537,624],{"class":554},[173,1539,246],{"class":219},[173,1541,1478],{"class":223},[173,1543,883],{"class":554},[173,1545,246],{"class":219},[173,1547,328],{"class":263},[173,1549,1550],{"class":223},").font ",[173,1552,246],{"class":219},[173,1554,728],{"class":223},[173,1556,1557],{"class":554},"italic",[173,1559,246],{"class":219},[173,1561,1562],{"class":263},"True",[173,1564,267],{"class":223},[173,1566,731],{"class":554},[173,1568,246],{"class":219},[173,1570,1571],{"class":182},"\"595959\"",[173,1573,595],{"class":223},[173,1575,1576],{"class":74,"line":451},[173,1577,237],{"emptyLinePlaceholder":236},[173,1579,1580,1583,1585,1588,1590],{"class":74,"line":491},[173,1581,1582],{"class":223},"summary_row ",[173,1584,246],{"class":219},[173,1586,1587],{"class":223}," legend_row ",[173,1589,1463],{"class":219},[173,1591,1592],{"class":263}," 1\n",[173,1594,1595,1597,1599,1601,1604,1606,1608,1610],{"class":74,"line":529},[173,1596,1471],{"class":223},[173,1598,624],{"class":554},[173,1600,246],{"class":219},[173,1602,1603],{"class":223},"summary_row, ",[173,1605,883],{"class":554},[173,1607,246],{"class":219},[173,1609,328],{"class":263},[173,1611,1612],{"class":223},",\n",[173,1614,1615,1618,1620,1622,1624,1626,1628,1630,1632,1635,1637,1639,1642,1645,1648,1650,1653,1655,1658,1660,1663],{"class":74,"line":535},[173,1616,1617],{"class":554},"        value",[173,1619,246],{"class":219},[173,1621,937],{"class":219},[173,1623,1164],{"class":182},[173,1625,944],{"class":943},[173,1627,947],{"class":263},[173,1629,950],{"class":223},[173,1631,953],{"class":943},[173,1633,1634],{"class":182}," issue(s) found in ",[173,1636,944],{"class":943},[173,1638,947],{"class":263},[173,1640,1641],{"class":223},"({i[",[173,1643,1644],{"class":182},"'row'",[173,1646,1647],{"class":223},"] ",[173,1649,787],{"class":219},[173,1651,1652],{"class":223}," i ",[173,1654,793],{"class":219},[173,1656,1657],{"class":223}," issues})",[173,1659,953],{"class":943},[173,1661,1662],{"class":182}," row(s)\"",[173,1664,595],{"class":223},[173,1666,1667],{"class":74,"line":540},[173,1668,237],{"emptyLinePlaceholder":236},[173,1670,1671,1674,1676],{"class":74,"line":571},[173,1672,1673],{"class":223},"ws.freeze_panes ",[173,1675,246],{"class":219},[173,1677,1678],{"class":182}," \"A2\"\n",[173,1680,1681,1684,1686,1689,1691,1694],{"class":74,"line":598},[173,1682,1683],{"class":223},"wb.active ",[173,1685,246],{"class":219},[173,1687,1688],{"class":223}," wb.index(wb[",[173,1690,608],{"class":182},[173,1692,1693],{"class":223},"])       ",[173,1695,1696],{"class":857},"# open on the issues list\n",[173,1698,1699,1701,1703],{"class":74,"line":891},[173,1700,921],{"class":223},[173,1702,549],{"class":182},[173,1704,595],{"class":223},[10,1706,1707],{},"Freezing the header keeps the column names visible while a reader scrolls to a highlighted row far down the sheet — a small thing that makes a 3,000-row file usable.",[158,1709,1711],{"id":1710},"clear-last-runs-marks-first","Clear last run's marks first",[10,1713,1714],{},"Highlights are ordinary formatting: they persist. A second run over a corrected file leaves yesterday's red cells in place unless you clear them:",[163,1716,1718],{"className":210,"code":1717,"language":212,"meta":168,"style":168},"from openpyxl import load_workbook\nfrom openpyxl.styles import PatternFill\n\nNO_FILL = PatternFill(fill_type=None)\n\ndef clear_highlights(ws, colours={\"FFC7CE\"}):\n    cleared = 0\n    for row in ws.iter_rows():\n        for cell in row:\n            fill = cell.fill\n            if fill is not None and fill.fill_type == \"solid\":\n                rgb = getattr(fill.start_color, \"rgb\", \"\") or \"\"\n                if rgb[-6:].upper() in colours:\n                    cell.fill = NO_FILL\n                    cell.comment = None\n                    cleared += 1\n    return cleared\n\nwb = load_workbook(\"marked.xlsx\")\nprint(\"cleared\", clear_highlights(wb[\"Orders\"]), \"old highlight(s)\")\nwb.save(\"marked.xlsx\")\n",[170,1719,1720,1730,1741,1745,1762,1766,1787,1797,1810,1822,1832,1861,1890,1911,1921,1931,1941,1949,1953,1965,1987],{"__ignoreMap":168},[173,1721,1722,1724,1726,1728],{"class":74,"line":175},[173,1723,642],{"class":219},[173,1725,645],{"class":223},[173,1727,220],{"class":219},[173,1729,650],{"class":223},[173,1731,1732,1734,1736,1738],{"class":74,"line":233},[173,1733,642],{"class":219},[173,1735,657],{"class":223},[173,1737,220],{"class":219},[173,1739,1740],{"class":223}," PatternFill\n",[173,1742,1743],{"class":74,"line":240},[173,1744,237],{"emptyLinePlaceholder":236},[173,1746,1747,1750,1752,1754,1756,1758,1760],{"class":74,"line":252},[173,1748,1749],{"class":263},"NO_FILL",[173,1751,686],{"class":219},[173,1753,689],{"class":223},[173,1755,711],{"class":554},[173,1757,246],{"class":219},[173,1759,381],{"class":263},[173,1761,595],{"class":223},[173,1763,1764],{"class":74,"line":301},[173,1765,237],{"emptyLinePlaceholder":236},[173,1767,1768,1771,1775,1778,1780,1782,1784],{"class":74,"line":342},[173,1769,1770],{"class":219},"def",[173,1772,1774],{"class":1773},"s_Opv"," clear_highlights",[173,1776,1777],{"class":223},"(ws, colours",[173,1779,246],{"class":219},[173,1781,944],{"class":223},[173,1783,697],{"class":182},[173,1785,1786],{"class":223},"}):\n",[173,1788,1789,1792,1794],{"class":74,"line":386},[173,1790,1791],{"class":223},"    cleared ",[173,1793,246],{"class":219},[173,1795,1796],{"class":263}," 0\n",[173,1798,1799,1802,1805,1807],{"class":74,"line":392},[173,1800,1801],{"class":219},"    for",[173,1803,1804],{"class":223}," row ",[173,1806,793],{"class":219},[173,1808,1809],{"class":223}," ws.iter_rows():\n",[173,1811,1812,1815,1817,1819],{"class":74,"line":397},[173,1813,1814],{"class":219},"        for",[173,1816,790],{"class":223},[173,1818,793],{"class":219},[173,1820,1821],{"class":223}," row:\n",[173,1823,1824,1827,1829],{"class":74,"line":408},[173,1825,1826],{"class":223},"            fill ",[173,1828,246],{"class":219},[173,1830,1831],{"class":223}," cell.fill\n",[173,1833,1834,1837,1840,1842,1845,1847,1850,1853,1856,1859],{"class":74,"line":451},[173,1835,1836],{"class":219},"            if",[173,1838,1839],{"class":223}," fill ",[173,1841,843],{"class":219},[173,1843,1844],{"class":219}," not",[173,1846,846],{"class":263},[173,1848,1849],{"class":219}," and",[173,1851,1852],{"class":223}," fill.fill_type ",[173,1854,1855],{"class":219},"==",[173,1857,1858],{"class":182}," \"solid\"",[173,1860,849],{"class":223},[173,1862,1863,1866,1868,1871,1874,1877,1879,1882,1884,1887],{"class":74,"line":491},[173,1864,1865],{"class":223},"                rgb ",[173,1867,246],{"class":219},[173,1869,1870],{"class":263}," getattr",[173,1872,1873],{"class":223},"(fill.start_color, ",[173,1875,1876],{"class":182},"\"rgb\"",[173,1878,267],{"class":223},[173,1880,1881],{"class":182},"\"\"",[173,1883,563],{"class":223},[173,1885,1886],{"class":219},"or",[173,1888,1889],{"class":182}," \"\"\n",[173,1891,1892,1895,1898,1900,1903,1906,1908],{"class":74,"line":529},[173,1893,1894],{"class":219},"                if",[173,1896,1897],{"class":223}," rgb[",[173,1899,369],{"class":219},[173,1901,1902],{"class":263},"6",[173,1904,1905],{"class":223},":].upper() ",[173,1907,793],{"class":219},[173,1909,1910],{"class":223}," colours:\n",[173,1912,1913,1916,1918],{"class":74,"line":535},[173,1914,1915],{"class":223},"                    cell.fill ",[173,1917,246],{"class":219},[173,1919,1920],{"class":263}," NO_FILL\n",[173,1922,1923,1926,1928],{"class":74,"line":540},[173,1924,1925],{"class":223},"                    cell.comment ",[173,1927,246],{"class":219},[173,1929,1930],{"class":263}," None\n",[173,1932,1933,1936,1939],{"class":74,"line":571},[173,1934,1935],{"class":223},"                    cleared ",[173,1937,1938],{"class":219},"+=",[173,1940,1592],{"class":263},[173,1942,1943,1946],{"class":74,"line":598},[173,1944,1945],{"class":219},"    return",[173,1947,1948],{"class":223}," cleared\n",[173,1950,1951],{"class":74,"line":891},[173,1952,237],{"emptyLinePlaceholder":236},[173,1954,1955,1957,1959,1961,1963],{"class":74,"line":902},[173,1956,747],{"class":223},[173,1958,246],{"class":219},[173,1960,752],{"class":223},[173,1962,549],{"class":182},[173,1964,595],{"class":223},[173,1966,1967,1969,1971,1974,1977,1979,1982,1985],{"class":74,"line":913},[173,1968,931],{"class":263},[173,1970,934],{"class":223},[173,1972,1973],{"class":182},"\"cleared\"",[173,1975,1976],{"class":223},", clear_highlights(wb[",[173,1978,582],{"class":182},[173,1980,1981],{"class":223},"]), ",[173,1983,1984],{"class":182},"\"old highlight(s)\"",[173,1986,595],{"class":223},[173,1988,1989,1991,1993],{"class":74,"line":918},[173,1990,921],{"class":223},[173,1992,549],{"class":182},[173,1994,595],{"class":223},[10,1996,1997,1998,2000,2001,2004,2005,2009],{},"Matching on the last six characters of the colour handles both ",[170,1999,963],{}," and the ",[170,2002,2003],{},"00FFC7CE"," form openpyxl sometimes returns. Clearing only ",[2006,2007,2008],"em",{},"your"," colour leaves the reader's own formatting alone, which matters when the same file is passed back and forth.",[158,2011,2013],{"id":2012},"static-fill-or-conditional-formatting","Static fill or conditional formatting?",[2015,2016,2017,2030],"table",{},[2018,2019,2020],"thead",{},[2021,2022,2023,2027],"tr",{},[2024,2025,2026],"th",{},"Situation",[2024,2028,2029],{},"Use",[2031,2032,2033,2046,2057,2065,2073],"tbody",{},[2021,2034,2035,2039],{},[2036,2037,2038],"td",{},"Python decided the value is wrong",[2036,2040,2041,2042,2045],{},"Static ",[170,2043,2044],{},"PatternFill"," — the reason lives in your code",[2021,2047,2048,2051],{},[2036,2049,2050],{},"The rule is simple and should keep reacting as the reader edits",[2036,2052,2053],{},[14,2054,2056],{"href":2055},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002F","Conditional formatting",[2021,2058,2059,2062],{},[2036,2060,2061],{},"The reason needs explaining",[2036,2063,2064],{},"Fill plus a comment",[2021,2066,2067,2070],{},[2036,2068,2069],{},"Failures must be counted or filtered",[2036,2071,2072],{},"An issues sheet, alongside either",[2021,2074,2075,2078],{},[2036,2076,2077],{},"The file will be re-submitted and re-checked",[2036,2079,2080],{},"Static fill, cleared at the start of each run",[10,2082,2083],{},"Conditional formatting has a real advantage — it updates the moment someone corrects the cell — but it can only express rules Excel can evaluate. A \"region is not in the master list\" rule that depends on a database lookup cannot live in the workbook, so the decision usually makes itself.",[158,2085,2087],{"id":2086},"colour-by-severity-not-just-by-failure","Colour by severity, not just by failure",[10,2089,2090],{},"One red for everything is fine on a short list. Once a file routinely carries dozens of issues, a second colour separates \"this must be fixed\" from \"check this\":",[20,2092,29,2097,29,2100,29,2103,29,2106,29,2110,29,2114,29,2120,29,2124,29,2128,29,2130,29,2134,29,2139,29,2142],{"viewBox":2093,"role":23,"ariaLabelledBy":2094,"xmlns":27,"style":28},"0 0 740 202",[2095,2096],"hi-sev-t","hi-sev-d",[31,2098,2099],{"id":2095},"Two highlight colours for two kinds of problem",[35,2101,2102],{"id":2096},"Red with dark red text marks rows that were rejected and must be corrected. Amber with brown text marks values that were accepted but look unusual and are worth a second look. Both use Excel's own bad and neutral cell colours.",[39,2104],{"x":41,"y":41,"width":42,"height":2105,"fill":44},"202",[39,2107],{"x":113,"y":113,"width":2108,"height":1360,"rx":2109,"fill":44,"stroke":53},"330","14",[39,2111],{"x":2112,"y":89,"width":2113,"height":48,"fill":1326},"54","282",[55,2115,2119],{"x":2116,"y":2117,"style":2118},"195","82","font-size:13px;font-weight:700;fill:#9C0006;text-anchor:middle","rejected — must be fixed",[55,2121,2123],{"x":2116,"y":2122,"style":64},"122","the row did not enter the report",[55,2125,2127],{"x":2116,"y":2126,"style":64},"146","fill FFC7CE · font 9C0006",[39,2129],{"x":1384,"y":113,"width":2108,"height":1360,"rx":2109,"fill":44,"stroke":53},[39,2131],{"x":2132,"y":89,"width":2113,"height":48,"fill":2133},"404","#FFEB9C",[55,2135,2138],{"x":2136,"y":2117,"style":2137},"545","font-size:13px;font-weight:700;fill:#5a4a00;text-anchor:middle","unusual — worth a look",[55,2140,2141],{"x":2136,"y":2122,"style":64},"the row was accepted as-is",[55,2143,2144],{"x":2136,"y":2126,"style":64},"fill FFEB9C · font 5A4A00",[10,2146,2147],{},"Both pairs are Excel's own built-in styles — \"Bad\" and \"Neutral\" — so the workbook keeps looking like a spreadsheet rather than a script's output. Carry the severity on the issue record itself and pick the fill from it, so the colour and the message can never disagree.",[158,2149,2151],{"id":2150},"common-pitfalls-and-gotchas","Common pitfalls and gotchas",[2015,2153,2154,2167],{},[2018,2155,2156],{},[2021,2157,2158,2161,2164],{},[2024,2159,2160],{},"Symptom",[2024,2162,2163],{},"Cause",[2024,2165,2166],{},"Fix",[2031,2168,2169,2183,2198,2212,2230,2241],{},[2021,2170,2171,2174,2177],{},[2036,2172,2173],{},"Highlights vanish after writing data",[2036,2175,2176],{},"pandas rewrote the sheet",[2036,2178,2179,2180],{},"Apply formatting after ",[170,2181,2182],{},"to_excel",[2021,2184,2185,2188,2191],{},[2036,2186,2187],{},"Wrong cells marked",[2036,2189,2190],{},"Off-by-one between DataFrame index and sheet row",[2036,2192,2193,2194,2197],{},"Store spreadsheet rows (",[170,2195,2196],{},"index + 2",") in the issue record",[2021,2199,2200,2203,2209],{},[2036,2201,2202],{},"Only one reason shows on a cell",[2036,2204,2205,2206,2208],{},"A second ",[170,2207,1264],{}," replaced the first",[2036,2210,2211],{},"Group problems by coordinate and join them",[2021,2213,2214,2217,2220],{},[2036,2215,2216],{},"Comment text is clipped",[2036,2218,2219],{},"Default comment box is small",[2036,2221,2222,2223,2226,2227],{},"Set ",[170,2224,2225],{},"comment.width"," and ",[170,2228,2229],{},"comment.height",[2021,2231,2232,2235,2238],{},[2036,2233,2234],{},"Old marks still present",[2036,2236,2237],{},"Previous run's fills were never cleared",[2036,2239,2240],{},"Clear your own colour at the start",[2021,2242,2243,2249,2252],{},[2036,2244,2245,2248],{},[170,2246,2247],{},"KeyError"," on a column name",[2036,2250,2251],{},"Issue refers to a column not on the sheet",[2036,2253,2254],{},"Look up the header map and skip unknown columns",[158,2256,2258],{"id":2257},"performance-and-scale-notes","Performance and scale notes",[10,2260,2261],{},"Fills and comments are per-cell objects, so marking tens of thousands of cells makes the file noticeably larger and slower to open — and a sheet where a third of the cells are red communicates nothing anyway. Cap the number of highlighted cells (the first 500, say), state the cap in the summary line, and let the issues sheet carry the full list. Clearing highlights walks every cell, so on a large sheet restrict the scan to the used range or to the columns you know your rules touch.",[158,2263,2265],{"id":2264},"conclusion","Conclusion",[10,2267,2268],{},"Highlighting is the step that turns validation from a log into a conversation. Write the data first, map issue columns to letters from the sheet's own header row, fill with Excel's native bad-cell colours, attach one comment per cell carrying every reason that applies, and add a legend so nothing needs explaining. Clear your own marks at the start of each run, cap the number of highlights on huge files, and keep the full list on an issues sheet.",[158,2270,2272],{"id":2271},"frequently-asked-questions","Frequently asked questions",[10,2274,2275,2279],{},[2276,2277,2278],"strong",{},"Should I highlight with a static fill or conditional formatting?","\nUse a static fill when Python decided the cell is wrong — the reason lives in your code, not in a formula. Use conditional formatting when the rule can be expressed in Excel and should keep reacting as the reader edits.",[10,2281,2282,2285],{},[2276,2283,2284],{},"Why did my highlight disappear after writing the DataFrame?","\npandas rewrote the sheet. Write the data first, then apply the formatting to the finished sheet.",[10,2287,2288,2291],{},[2276,2289,2290],{},"How do I show why a cell is wrong?","\nAttach a comment to the cell with the rule that failed, and keep the same text in an issues sheet so the reasons can be filtered and counted.",[10,2293,2294,2297,2298,2300],{},[2276,2295,2296],{},"Do highlighted cells survive a round trip through Excel?","\nYes. A ",[170,2299,2044],{}," is ordinary cell formatting, so it stays until someone clears it — which is why a run should clear its previous highlights before applying new ones.",[158,2302,2304],{"id":2303},"related","Related",[10,2306,2307],{},"Up to the parent guide:",[2309,2310,2311],"ul",{},[2312,2313,2314,2316],"li",{},[14,2315,17],{"href":16}," — where the issue records come from.",[10,2318,2319],{},"Related guides:",[2309,2321,2322,2329,2335,2342],{},[2312,2323,2324,2328],{},[14,2325,2327],{"href":2326},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcheck-excel-data-types-with-pandas\u002F","Check Excel Data Types with pandas"," — producing the failures this page paints.",[2312,2330,2331,2334],{},[14,2332,2333],{"href":2055},"Applying Conditional Formatting with openpyxl"," — rules that keep reacting as the reader edits.",[2312,2336,2337,2341],{},[14,2338,2340],{"href":2339},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002F","Styling Excel Cells with openpyxl"," — fills, fonts and borders in depth.",[2312,2343,2344,2348],{},[14,2345,2347],{"href":2346},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fadd-dropdown-data-validation-to-excel-with-openpyxl\u002F","Add Dropdown Data Validation to Excel with openpyxl"," — preventing the errors you are highlighting.",[2350,2351,2352],"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 .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}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":168,"searchDepth":233,"depth":233,"links":2354},[2355,2356,2357,2358,2359,2360,2361,2362,2363,2364,2365,2366,2367],{"id":160,"depth":233,"text":161},{"id":203,"depth":233,"text":204},{"id":628,"depth":233,"text":629},{"id":975,"depth":233,"text":976},{"id":1389,"depth":233,"text":1390},{"id":1710,"depth":233,"text":1711},{"id":2012,"depth":233,"text":2013},{"id":2086,"depth":233,"text":2087},{"id":2150,"depth":233,"text":2151},{"id":2257,"depth":233,"text":2258},{"id":2264,"depth":233,"text":2265},{"id":2271,"depth":233,"text":2272},{"id":2303,"depth":233,"text":2304},"2026-08-01","Mark the exact cells that failed validation with openpyxl — a red fill, a comment explaining the rule, a legend and an issues sheet — so a submitter can fix the file in minutes.","md",[2372,2374,2376,2378],{"q":2278,"a":2373},"Use a static fill when Python decided the cell is wrong — the reason lives in your code, not in a formula. Use conditional formatting when the rule can be expressed in Excel and should keep reacting as the reader edits.",{"q":2284,"a":2375},"pandas rewrote the sheet. Write the data first, then apply the formatting to the finished sheet.",{"q":2290,"a":2377},"Attach a comment to the cell with the rule that failed, and keep the same text in an issues sheet so the reasons can be filtered and counted.",{"q":2296,"a":2379},"Yes. A PatternFill is ordinary cell formatting, so it stays until someone clears it — which is why a run should clear its previous highlights before applying new ones.",{"breadcrumb":2381},[2382,2385,2388,2389],{"name":2383,"item":2384},"Home","\u002F",{"name":2386,"item":2387},"Advanced Data Transformation and Cleaning","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":17,"item":16},{"name":5,"item":2390},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fhighlight-invalid-cells-in-excel-with-python\u002F","\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fhighlight-invalid-cells-in-excel-with-python",{"title":5,"description":2393},"Turn a list of validation failures into visible Excel output: PatternFill on the offending cells, comments with the reason, a legend, a filterable issues sheet and a summary count.","highlight-invalid-cells-in-excel-with-python","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fhighlight-invalid-cells-in-excel-with-python\u002Findex","how-to","Lq-4Voi80iExwNlKEeKkd0QrLm_NXqRKi0PDySkyi_o",[2399,2403],{"title":2400,"path":2401,"stem":2402,"children":-1},"Find Duplicate Rows in Excel with Python","\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Ffind-duplicate-rows-in-excel-with-python","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Ffind-duplicate-rows-in-excel-with-python\u002Findex",{"title":2404,"path":2405,"stem":2406,"children":-1},"Validate Excel Columns Before Import with pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-columns-before-import-with-pandas","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-columns-before-import-with-pandas\u002Findex",1785584462739]