[{"data":1,"prerenderedAt":2230},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-dropdown-lists-from-an-existing-workbook":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-dropdown-lists-from-an-existing-workbook":2196,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-dropdown-lists-from-an-existing-workbook":2205,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-dropdown-lists-from-an-existing-workbook":2210,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-dropdown-lists-from-an-existing-workbook":2211},{"id":4,"title":5,"body":6,"dateModified":2161,"datePublished":2161,"description":2162,"extension":2163,"faq":2164,"keyword":2173,"kind":2174,"meta":2175,"navigation":265,"path":2184,"seo":2185,"slug":2188,"stem":2189,"topics":2190,"type":2194,"__hash__":2195},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-dropdown-lists-from-an-existing-workbook\u002Findex.md","Read Dropdown Lists and Validation Rules from an Existing Workbook",{"type":7,"value":8,"toc":2147},"minimark",[9,19,186,191,220,226,230,237,402,421,425,439,845,863,973,977,980,1377,1399,1403,1406,1567,1575,1775,1779,1782,1985,1989,2009,2013,2058,2062,2069,2073,2082,2086,2092,2098,2104,2110,2114,2143],[10,11,12,13,18],"p",{},"A template's dropdowns encode business rules: the regions an order can belong to, the statuses a ticket may have, the priority codes finance recognises. When a script reads the filled-in template, those rules are useful twice over. They document what the valid values are — without hard-coding a copy that goes stale when someone edits the list — and they let the script check what people actually entered, because Excel only enforces a dropdown at the moment a value is typed; anything pasted in, imported or entered before the rule existed can violate it freely. This page, part of ",[14,15,17],"a",{"href":16},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002F","Extracting Data from Messy Excel Sheets",", reads the rules with openpyxl, resolves each to its allowed values and reports every cell that breaks one.",[20,21,29,30,29,34,29,38,29,46,29,56,29,67,29,74,29,79,29,87,29,94,29,100,29,105,29,108,29,113,29,116,29,122,29,126,29,130,29,133,29,136,29,139,29,145,29,149,29,153,29,156,29,159,29,164,29,171,29,176,29,180,29,183],"svg",{"viewBox":22,"role":23,"ariaLabelledBy":24,"xmlns":27,"style":28},"4 0 752 436","img",[25,26],"dv-sources-t","dv-sources-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  ",[31,32,33],"title",{"id":25},"Where a dropdown's values can come from",[35,36,37],"desc",{"id":26},"A data validation rule fans out to four kinds of source. An inline list stores the values in the rule itself, such as open, paid, void. A range reference points at cells on another sheet, such as Lists A1 to A3. A named range points at cells through a defined name, such as Priorities. A number or date rule stores limits instead of a list, such as a whole number between 1 and 500.",[39,40],"rect",{"x":41,"y":42,"width":43,"height":44,"fill":45},"4","0","752","436","#ffffff",[39,47],{"x":48,"y":49,"width":50,"height":51,"rx":52,"fill":53,"stroke":54,"style":55},"20.0","171.5","220.0","93.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[57,58,60],"g",{"transform":59},"translate(118.0,184.5) scale(1.000)",[61,62],"path",{"d":63,"fill":64,"stroke":65,"style":66},"M3 5h18l-7 8v6l-4 2v-8z","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[68,69,73],"text",{"x":70,"y":71,"style":72},"130.0","231.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","ws.data_validations",[68,75,78],{"x":70,"y":76,"style":77},"250.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","one rule per dropdown",[39,80],{"x":81,"y":82,"width":83,"height":84,"rx":52,"fill":85,"stroke":86,"style":55},"460.0","16.0","280.0","92.0","#d9f4f1","var(--teal,#0f9488)",[57,88,90],{"transform":89},"translate(588.0,29.0) scale(1.000)",[61,91],{"d":92,"fill":64,"stroke":93,"style":66},"M5 9h14 M4 15h14 M10 4l-2 16 M16 4l-2 16","var(--teal-ink,#0b6157)",[68,95,99],{"x":96,"y":97,"style":98},"600.0","75.0","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Inline list",[68,101,104],{"x":96,"y":102,"style":103},"94.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","\"open,paid,void\"",[61,106],{"d":107,"fill":64,"stroke":54,"style":55},"M244.0,218.0 C349.0,218.0 349.0,62.0 450.0,62.0",[109,110],"polygon",{"points":111,"fill":112},"454.0,62.0 445.0,67.0 445.0,57.0","#5b5cf0",[39,114],{"x":81,"y":115,"width":83,"height":84,"rx":52,"fill":85,"stroke":86,"style":55},"120.0",[57,117,119],{"transform":118},"translate(588.0,133.0) scale(1.000)",[61,120],{"d":121,"fill":64,"stroke":93,"style":66},"M4 4h16v16H4z M4 9h16 M4 14h16 M10 4v16",[68,123,125],{"x":96,"y":124,"style":98},"179.0","Range on another sheet",[68,127,129],{"x":96,"y":128,"style":103},"198.0","Lists!$A$1:$A$3",[61,131],{"d":132,"fill":64,"stroke":54,"style":55},"M244.0,218.0 C349.0,218.0 349.0,166.0 450.0,166.0",[109,134],{"points":135,"fill":112},"454.0,166.0 445.0,171.0 445.0,161.0",[39,137],{"x":81,"y":138,"width":83,"height":84,"rx":52,"fill":85,"stroke":86,"style":55},"224.0",[57,140,142],{"transform":141},"translate(588.0,237.0) scale(1.000)",[61,143],{"d":144,"fill":64,"stroke":93,"style":66},"M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1 M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1",[68,146,148],{"x":96,"y":147,"style":98},"283.0","Named range",[68,150,152],{"x":96,"y":151,"style":103},"302.0","Priorities → Lists!$B$1:$B$3",[61,154],{"d":155,"fill":64,"stroke":54,"style":55},"M244.0,218.0 C349.0,218.0 349.0,270.0 450.0,270.0",[109,157],{"points":158,"fill":112},"454.0,270.0 445.0,275.0 445.0,265.0",[39,160],{"x":81,"y":161,"width":83,"height":84,"rx":52,"fill":162,"stroke":163,"style":55},"328.0","#fdefd8","var(--gold,#b4740a)",[57,165,167],{"transform":166},"translate(588.0,341.0) scale(1.000)",[61,168],{"d":169,"fill":64,"stroke":170,"style":66},"M12 9a3 3 0 1 0 .01 0z M12 2.5v3 M12 18.5v3 M2.5 12h3 M18.5 12h3 M5.3 5.3l2.1 2.1 M16.6 16.6l2.1 2.1 M5.3 18.7l2.1-2.1 M16.6 7.4l2.1-2.1","var(--gold-ink,#7a4e06)",[68,172,175],{"x":96,"y":173,"style":174},"387.0","font-size:13px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Number or date limits",[68,177,179],{"x":96,"y":178,"style":103},"406.0","whole, between 1 and 500",[61,181],{"d":182,"fill":64,"stroke":54,"style":55},"M244.0,218.0 C349.0,218.0 349.0,374.0 450.0,374.0",[109,184],{"points":185,"fill":112},"454.0,374.0 445.0,379.0 445.0,369.0",[187,188,190],"h2",{"id":189},"prerequisites","Prerequisites",[192,193,198],"pre",{"className":194,"code":195,"language":196,"meta":197,"style":197},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl pandas\n","bash","",[199,200,201],"code",{"__ignoreMap":197},[202,203,206,210,214,217],"span",{"class":204,"line":205},"line",1,[202,207,209],{"class":208},"sMTad","pip",[202,211,213],{"class":212},"srMev"," install",[202,215,216],{"class":212}," openpyxl",[202,218,219],{"class":212}," pandas\n",[10,221,222,225],{},[199,223,224],{},"orders_template.xlsx"," has an Orders sheet with four validated columns — region from a range on a Lists sheet, status from an inline list, priority from a named range, and quantity limited to whole numbers from 1 to 500 — and four rows of entered data, some of which break the rules.",[187,227,229],{"id":228},"step-1-list-the-validation-rules","Step 1 — List the validation rules",[10,231,232,233,236],{},"Every rule on a sheet lives in the worksheet's ",[199,234,235],{},"data_validations.dataValidation"," list — one entry per rule. Print each rule's type, source and the cells it covers:",[192,238,242],{"className":239,"code":240,"language":241,"meta":197,"style":197},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import load_workbook\n\nwb = load_workbook(\"orders_template.xlsx\")\nws = wb[\"Orders\"]\nfor dv in ws.data_validations.dataValidation:\n    print(f\"{dv.type:6} cells={str(dv.sqref):9} formula1={dv.formula1!r:24} formula2={dv.formula2!r} blank_ok={dv.allow_blank}\")\n","python",[199,243,244,260,267,285,302,317],{"__ignoreMap":197},[202,245,246,250,254,257],{"class":204,"line":205},[202,247,249],{"class":248},"s-kum","from",[202,251,253],{"class":252},"skGVy"," openpyxl ",[202,255,256],{"class":248},"import",[202,258,259],{"class":252}," load_workbook\n",[202,261,263],{"class":204,"line":262},2,[202,264,266],{"emptyLinePlaceholder":265},true,"\n",[202,268,270,273,276,279,282],{"class":204,"line":269},3,[202,271,272],{"class":252},"wb ",[202,274,275],{"class":248},"=",[202,277,278],{"class":252}," load_workbook(",[202,280,281],{"class":212},"\"orders_template.xlsx\"",[202,283,284],{"class":252},")\n",[202,286,288,291,293,296,299],{"class":204,"line":287},4,[202,289,290],{"class":252},"ws ",[202,292,275],{"class":248},[202,294,295],{"class":252}," wb[",[202,297,298],{"class":212},"\"Orders\"",[202,300,301],{"class":252},"]\n",[202,303,305,308,311,314],{"class":204,"line":304},5,[202,306,307],{"class":248},"for",[202,309,310],{"class":252}," dv ",[202,312,313],{"class":248},"in",[202,315,316],{"class":252}," ws.data_validations.dataValidation:\n",[202,318,320,324,327,330,333,337,340,343,346,349,351,354,357,360,362,365,367,370,373,375,378,380,383,386,388,391,393,396,398,400],{"class":204,"line":319},6,[202,321,323],{"class":322},"sP0c6","    print",[202,325,326],{"class":252},"(",[202,328,329],{"class":248},"f",[202,331,332],{"class":212},"\"",[202,334,336],{"class":335},"sSjpA","{",[202,338,339],{"class":252},"dv.type",[202,341,342],{"class":248},":6",[202,344,345],{"class":335},"}",[202,347,348],{"class":212}," cells=",[202,350,336],{"class":335},[202,352,353],{"class":322},"str",[202,355,356],{"class":252},"(dv.sqref)",[202,358,359],{"class":248},":9",[202,361,345],{"class":335},[202,363,364],{"class":212}," formula1=",[202,366,336],{"class":335},[202,368,369],{"class":252},"dv.formula1",[202,371,372],{"class":248},"!r:24",[202,374,345],{"class":335},[202,376,377],{"class":212}," formula2=",[202,379,336],{"class":335},[202,381,382],{"class":252},"dv.formula2",[202,384,385],{"class":248},"!r",[202,387,345],{"class":335},[202,389,390],{"class":212}," blank_ok=",[202,392,336],{"class":335},[202,394,395],{"class":252},"dv.allow_blank",[202,397,345],{"class":335},[202,399,332],{"class":212},[202,401,284],{"class":252},[10,403,404,405,408,409,412,413,416,417,420],{},"Four rules, one per column. ",[199,406,407],{},"sqref"," is the range the rule applies to — often far longer than the data, because templates validate empty rows ahead of time. ",[199,410,411],{},"formula1"," holds the list source or the lower limit, ",[199,414,415],{},"formula2"," the upper limit for \"between\" rules, and ",[199,418,419],{},"allow_blank"," whether an empty cell is acceptable.",[187,422,424],{"id":423},"step-2-resolve-each-list-to-its-values","Step 2 — Resolve each list to its values",[10,426,427,428,430,431,435,436,438],{},"A list rule's ",[199,429,411],{}," comes in three shapes, and each needs resolving differently. A quoted, comma-separated string ",[432,433,434],"em",{},"is"," the list. A reference such as ",[199,437,129],{}," must be read from that sheet. A bare name refers to a defined name, whose destinations give the range:",[192,440,442],{"className":239,"code":441,"language":241,"meta":197,"style":197},"from openpyxl.utils.cell import range_boundaries\n\ndef read_range(wb, sheet, ref):\n    min_col, min_row, max_col, max_row = range_boundaries(ref.replace(\"$\", \"\"))\n    cells = wb[sheet].iter_rows(min_row=min_row, max_row=max_row, min_col=min_col, max_col=max_col, values_only=True)\n    return [v for row in cells for v in row if v is not None]\n\ndef list_values(wb, formula):\n    f = formula.lstrip(\"=\")\n    if f.startswith('\"') and f.endswith('\"'):\n        return [v.strip() for v in f.strip('\"').split(\",\")]\n    if \"!\" in f:\n        sheet, ref = f.rsplit(\"!\", 1)\n        return read_range(wb, sheet.strip(\"'\"), ref)\n    if f in wb.defined_names:\n        return [v for sheet, ref in wb.defined_names[f].destinations for v in read_range(wb, sheet, ref)]\n    raise ValueError(f\"cannot resolve list source {formula!r}\")\n\nfor dv in ws.data_validations.dataValidation:\n    if dv.type == \"list\":\n        print(str(dv.sqref), \"->\", list_values(wb, dv.formula1))\n",[199,443,444,456,460,472,494,547,589,594,605,621,647,676,690,711,725,738,764,793,798,809,826],{"__ignoreMap":197},[202,445,446,448,451,453],{"class":204,"line":205},[202,447,249],{"class":248},[202,449,450],{"class":252}," openpyxl.utils.cell ",[202,452,256],{"class":248},[202,454,455],{"class":252}," range_boundaries\n",[202,457,458],{"class":204,"line":262},[202,459,266],{"emptyLinePlaceholder":265},[202,461,462,465,469],{"class":204,"line":269},[202,463,464],{"class":248},"def",[202,466,468],{"class":467},"s_Opv"," read_range",[202,470,471],{"class":252},"(wb, sheet, ref):\n",[202,473,474,477,479,482,485,488,491],{"class":204,"line":287},[202,475,476],{"class":252},"    min_col, min_row, max_col, max_row ",[202,478,275],{"class":248},[202,480,481],{"class":252}," range_boundaries(ref.replace(",[202,483,484],{"class":212},"\"$\"",[202,486,487],{"class":252},", ",[202,489,490],{"class":212},"\"\"",[202,492,493],{"class":252},"))\n",[202,495,496,499,501,504,508,510,513,516,518,521,524,526,529,532,534,537,540,542,545],{"class":204,"line":304},[202,497,498],{"class":252},"    cells ",[202,500,275],{"class":248},[202,502,503],{"class":252}," wb[sheet].iter_rows(",[202,505,507],{"class":506},"sa561","min_row",[202,509,275],{"class":248},[202,511,512],{"class":252},"min_row, ",[202,514,515],{"class":506},"max_row",[202,517,275],{"class":248},[202,519,520],{"class":252},"max_row, ",[202,522,523],{"class":506},"min_col",[202,525,275],{"class":248},[202,527,528],{"class":252},"min_col, ",[202,530,531],{"class":506},"max_col",[202,533,275],{"class":248},[202,535,536],{"class":252},"max_col, ",[202,538,539],{"class":506},"values_only",[202,541,275],{"class":248},[202,543,544],{"class":322},"True",[202,546,284],{"class":252},[202,548,549,552,555,557,560,562,565,567,570,572,574,577,579,581,584,587],{"class":204,"line":319},[202,550,551],{"class":248},"    return",[202,553,554],{"class":252}," [v ",[202,556,307],{"class":248},[202,558,559],{"class":252}," row ",[202,561,313],{"class":248},[202,563,564],{"class":252}," cells ",[202,566,307],{"class":248},[202,568,569],{"class":252}," v ",[202,571,313],{"class":248},[202,573,559],{"class":252},[202,575,576],{"class":248},"if",[202,578,569],{"class":252},[202,580,434],{"class":248},[202,582,583],{"class":248}," not",[202,585,586],{"class":322}," None",[202,588,301],{"class":252},[202,590,592],{"class":204,"line":591},7,[202,593,266],{"emptyLinePlaceholder":265},[202,595,597,599,602],{"class":204,"line":596},8,[202,598,464],{"class":248},[202,600,601],{"class":467}," list_values",[202,603,604],{"class":252},"(wb, formula):\n",[202,606,608,611,613,616,619],{"class":204,"line":607},9,[202,609,610],{"class":252},"    f ",[202,612,275],{"class":248},[202,614,615],{"class":252}," formula.lstrip(",[202,617,618],{"class":212},"\"=\"",[202,620,284],{"class":252},[202,622,624,627,630,633,636,639,642,644],{"class":204,"line":623},10,[202,625,626],{"class":248},"    if",[202,628,629],{"class":252}," f.startswith(",[202,631,632],{"class":212},"'\"'",[202,634,635],{"class":252},") ",[202,637,638],{"class":248},"and",[202,640,641],{"class":252}," f.endswith(",[202,643,632],{"class":212},[202,645,646],{"class":252},"):\n",[202,648,650,653,656,658,660,662,665,667,670,673],{"class":204,"line":649},11,[202,651,652],{"class":248},"        return",[202,654,655],{"class":252}," [v.strip() ",[202,657,307],{"class":248},[202,659,569],{"class":252},[202,661,313],{"class":248},[202,663,664],{"class":252}," f.strip(",[202,666,632],{"class":212},[202,668,669],{"class":252},").split(",[202,671,672],{"class":212},"\",\"",[202,674,675],{"class":252},")]\n",[202,677,679,681,684,687],{"class":204,"line":678},12,[202,680,626],{"class":248},[202,682,683],{"class":212}," \"!\"",[202,685,686],{"class":248}," in",[202,688,689],{"class":252}," f:\n",[202,691,693,696,698,701,704,706,709],{"class":204,"line":692},13,[202,694,695],{"class":252},"        sheet, ref ",[202,697,275],{"class":248},[202,699,700],{"class":252}," f.rsplit(",[202,702,703],{"class":212},"\"!\"",[202,705,487],{"class":252},[202,707,708],{"class":322},"1",[202,710,284],{"class":252},[202,712,714,716,719,722],{"class":204,"line":713},14,[202,715,652],{"class":248},[202,717,718],{"class":252}," read_range(wb, sheet.strip(",[202,720,721],{"class":212},"\"'\"",[202,723,724],{"class":252},"), ref)\n",[202,726,728,730,733,735],{"class":204,"line":727},15,[202,729,626],{"class":248},[202,731,732],{"class":252}," f ",[202,734,313],{"class":248},[202,736,737],{"class":252}," wb.defined_names:\n",[202,739,741,743,745,747,750,752,755,757,759,761],{"class":204,"line":740},16,[202,742,652],{"class":248},[202,744,554],{"class":252},[202,746,307],{"class":248},[202,748,749],{"class":252}," sheet, ref ",[202,751,313],{"class":248},[202,753,754],{"class":252}," wb.defined_names[f].destinations ",[202,756,307],{"class":248},[202,758,569],{"class":252},[202,760,313],{"class":248},[202,762,763],{"class":252}," read_range(wb, sheet, ref)]\n",[202,765,767,770,773,775,777,780,782,785,787,789,791],{"class":204,"line":766},17,[202,768,769],{"class":248},"    raise",[202,771,772],{"class":322}," ValueError",[202,774,326],{"class":252},[202,776,329],{"class":248},[202,778,779],{"class":212},"\"cannot resolve list source ",[202,781,336],{"class":335},[202,783,784],{"class":252},"formula",[202,786,385],{"class":248},[202,788,345],{"class":335},[202,790,332],{"class":212},[202,792,284],{"class":252},[202,794,796],{"class":204,"line":795},18,[202,797,266],{"emptyLinePlaceholder":265},[202,799,801,803,805,807],{"class":204,"line":800},19,[202,802,307],{"class":248},[202,804,310],{"class":252},[202,806,313],{"class":248},[202,808,316],{"class":252},[202,810,812,814,817,820,823],{"class":204,"line":811},20,[202,813,626],{"class":248},[202,815,816],{"class":252}," dv.type ",[202,818,819],{"class":248},"==",[202,821,822],{"class":212}," \"list\"",[202,824,825],{"class":252},":\n",[202,827,829,832,834,836,839,842],{"class":204,"line":828},21,[202,830,831],{"class":322},"        print",[202,833,326],{"class":252},[202,835,353],{"class":322},[202,837,838],{"class":252},"(dv.sqref), ",[202,840,841],{"class":212},"\"->\"",[202,843,844],{"class":252},", list_values(wb, dv.formula1))\n",[10,846,847,848,850,851,854,855,858,859,862],{},"Each column now has its real allowed values, read from wherever the template keeps them. When someone adds \"West\" to the Lists sheet, the script picks it up on the next run without a code change. Formulas saved by Excel usually omit the leading ",[199,849,275],{},", while some tools include it; ",[199,852,853],{},"lstrip(\"=\")"," handles both. Rules whose source is a formula — ",[199,856,857],{},"INDIRECT"," for dependent dropdowns, ",[199,860,861],{},"OFFSET"," for dynamic ranges — cannot be resolved without calculating the workbook, and the resolver raises rather than guessing.",[20,864,29,869,29,872,29,875,29,878,29,884,29,889,29,895,29,898,29,902,29,907,29,912,29,914,29,917,29,919,29,922,29,925,29,929,29,931,29,934,29,936,29,939,29,942,29,945,29,947,29,951,29,953,29,956,29,959,29,963,29,965,29,968,29,970],{"viewBox":865,"role":23,"ariaLabelledBy":866,"xmlns":27,"style":28},"4 0 752 208",[867,868],"dv-resolve-t","dv-resolve-d",[31,870,871],{"id":867},"How each kind of list source is resolved",[35,873,874],{"id":868},"A table of list sources. A quoted string such as \"open,paid,void\" is split on commas. A sheet reference such as Lists!$A$1:$A$3 is read from that sheet with iter_rows. A defined name such as Priorities is looked up in wb.defined_names and its destinations are read. A formula such as INDIRECT or OFFSET cannot be resolved without calculating the workbook.",[39,876],{"x":41,"y":42,"width":43,"height":877,"fill":45},"208",[39,879],{"x":48,"y":82,"width":880,"height":881,"rx":42,"fill":45,"stroke":882,"style":883},"152.1","40.0","var(--line,#cdd5e6)","stroke-width:1px",[39,885],{"x":886,"y":82,"width":887,"height":881,"rx":42,"fill":53,"stroke":54,"style":888},"172.1","284.0","stroke-width:1.2px",[68,890,894],{"x":891,"y":892,"style":893},"314.1","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","formula1 looks like",[39,896],{"x":897,"y":82,"width":887,"height":881,"rx":42,"fill":53,"stroke":54,"style":888},"456.0",[68,899,901],{"x":900,"y":892,"style":893},"598.0","Resolved by",[39,903],{"x":48,"y":904,"width":880,"height":905,"rx":42,"fill":906,"stroke":882,"style":883},"56.0","34.0","#e7ebef",[68,908,99],{"x":909,"y":910,"style":911},"32.0","78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start",[39,913],{"x":886,"y":904,"width":887,"height":905,"rx":42,"fill":45,"stroke":882,"style":883},[68,915,104],{"x":891,"y":916,"style":77},"77.0",[39,918],{"x":897,"y":904,"width":887,"height":905,"rx":42,"fill":45,"stroke":882,"style":883},[68,920,921],{"x":900,"y":916,"style":77},"split on commas",[39,923],{"x":48,"y":924,"width":880,"height":905,"rx":42,"fill":906,"stroke":882,"style":883},"90.0",[68,926,928],{"x":909,"y":927,"style":911},"112.0","Range reference",[39,930],{"x":886,"y":924,"width":887,"height":905,"rx":42,"fill":45,"stroke":882,"style":883},[68,932,129],{"x":891,"y":933,"style":77},"111.0",[39,935],{"x":897,"y":924,"width":887,"height":905,"rx":42,"fill":45,"stroke":882,"style":883},[68,937,938],{"x":900,"y":933,"style":77},"read the cells",[39,940],{"x":48,"y":941,"width":880,"height":905,"rx":42,"fill":906,"stroke":882,"style":883},"124.0",[68,943,148],{"x":909,"y":944,"style":911},"146.0",[39,946],{"x":886,"y":941,"width":887,"height":905,"rx":42,"fill":45,"stroke":882,"style":883},[68,948,950],{"x":891,"y":949,"style":77},"145.0","Priorities",[39,952],{"x":897,"y":941,"width":887,"height":905,"rx":42,"fill":45,"stroke":882,"style":883},[68,954,955],{"x":900,"y":949,"style":77},"defined_names → destinations",[39,957],{"x":48,"y":958,"width":880,"height":905,"rx":42,"fill":906,"stroke":882,"style":883},"158.0",[68,960,962],{"x":909,"y":961,"style":911},"180.0","INDIRECT \u002F OFFSET",[39,964],{"x":886,"y":958,"width":887,"height":905,"rx":42,"fill":45,"stroke":882,"style":883},[68,966,967],{"x":891,"y":124,"style":77},"INDIRECT(A2)",[39,969],{"x":897,"y":958,"width":887,"height":905,"rx":42,"fill":45,"stroke":882,"style":883},[68,971,972],{"x":900,"y":124,"style":77},"needs calculation — report",[187,974,976],{"id":975},"step-3-check-the-entered-data-against-every-rule","Step 3 — Check the entered data against every rule",[10,978,979],{},"With each rule resolved, walk the cells each rule covers and compare the value with what the rule allows. Number rules compare against their limits; list rules against their values; blanks are allowed only where the rule says so:",[192,981,983],{"className":239,"code":982,"language":241,"meta":197,"style":197},"def check_rules(wb, ws):\n    problems = []\n    for dv in ws.data_validations.dataValidation:\n        allowed = list_values(wb, dv.formula1) if dv.type == \"list\" else None\n        for cell_range in dv.sqref.ranges:\n            for row in ws.iter_rows(min_row=cell_range.min_row, max_row=min(cell_range.max_row, ws.max_row),\n                                    min_col=cell_range.min_col, max_col=cell_range.max_col):\n                for cell in row:\n                    v = cell.value\n                    if v is None:\n                        if not dv.allow_blank:\n                            problems.append((cell.coordinate, v, \"blank not allowed\"))\n                        continue\n                    if dv.type == \"list\" and v not in allowed:\n                        problems.append((cell.coordinate, v, f\"not in {allowed}\"))\n                    if dv.type == \"whole\" and dv.operator == \"between\":\n                        low, high = int(dv.formula1), int(dv.formula2)\n                        if not (isinstance(v, int) and low \u003C= v \u003C= high):\n                            problems.append((cell.coordinate, v, f\"not a whole number {low}-{high}\"))\n    return problems\n\nfor coord, value, reason in check_rules(wb, ws):\n    print(f\"{coord:5} {value!r:10} {reason}\")\n",[199,984,985,995,1005,1016,1040,1053,1082,1099,1112,1122,1135,1145,1155,1160,1183,1204,1227,1246,1280,1310,1317,1321,1334],{"__ignoreMap":197},[202,986,987,989,992],{"class":204,"line":205},[202,988,464],{"class":248},[202,990,991],{"class":467}," check_rules",[202,993,994],{"class":252},"(wb, ws):\n",[202,996,997,1000,1002],{"class":204,"line":262},[202,998,999],{"class":252},"    problems ",[202,1001,275],{"class":248},[202,1003,1004],{"class":252}," []\n",[202,1006,1007,1010,1012,1014],{"class":204,"line":269},[202,1008,1009],{"class":248},"    for",[202,1011,310],{"class":252},[202,1013,313],{"class":248},[202,1015,316],{"class":252},[202,1017,1018,1021,1023,1026,1028,1030,1032,1034,1037],{"class":204,"line":287},[202,1019,1020],{"class":252},"        allowed ",[202,1022,275],{"class":248},[202,1024,1025],{"class":252}," list_values(wb, dv.formula1) ",[202,1027,576],{"class":248},[202,1029,816],{"class":252},[202,1031,819],{"class":248},[202,1033,822],{"class":212},[202,1035,1036],{"class":248}," else",[202,1038,1039],{"class":322}," None\n",[202,1041,1042,1045,1048,1050],{"class":204,"line":304},[202,1043,1044],{"class":248},"        for",[202,1046,1047],{"class":252}," cell_range ",[202,1049,313],{"class":248},[202,1051,1052],{"class":252}," dv.sqref.ranges:\n",[202,1054,1055,1058,1060,1062,1065,1067,1069,1072,1074,1076,1079],{"class":204,"line":319},[202,1056,1057],{"class":248},"            for",[202,1059,559],{"class":252},[202,1061,313],{"class":248},[202,1063,1064],{"class":252}," ws.iter_rows(",[202,1066,507],{"class":506},[202,1068,275],{"class":248},[202,1070,1071],{"class":252},"cell_range.min_row, ",[202,1073,515],{"class":506},[202,1075,275],{"class":248},[202,1077,1078],{"class":322},"min",[202,1080,1081],{"class":252},"(cell_range.max_row, ws.max_row),\n",[202,1083,1084,1087,1089,1092,1094,1096],{"class":204,"line":591},[202,1085,1086],{"class":506},"                                    min_col",[202,1088,275],{"class":248},[202,1090,1091],{"class":252},"cell_range.min_col, ",[202,1093,531],{"class":506},[202,1095,275],{"class":248},[202,1097,1098],{"class":252},"cell_range.max_col):\n",[202,1100,1101,1104,1107,1109],{"class":204,"line":596},[202,1102,1103],{"class":248},"                for",[202,1105,1106],{"class":252}," cell ",[202,1108,313],{"class":248},[202,1110,1111],{"class":252}," row:\n",[202,1113,1114,1117,1119],{"class":204,"line":607},[202,1115,1116],{"class":252},"                    v ",[202,1118,275],{"class":248},[202,1120,1121],{"class":252}," cell.value\n",[202,1123,1124,1127,1129,1131,1133],{"class":204,"line":623},[202,1125,1126],{"class":248},"                    if",[202,1128,569],{"class":252},[202,1130,434],{"class":248},[202,1132,586],{"class":322},[202,1134,825],{"class":252},[202,1136,1137,1140,1142],{"class":204,"line":649},[202,1138,1139],{"class":248},"                        if",[202,1141,583],{"class":248},[202,1143,1144],{"class":252}," dv.allow_blank:\n",[202,1146,1147,1150,1153],{"class":204,"line":678},[202,1148,1149],{"class":252},"                            problems.append((cell.coordinate, v, ",[202,1151,1152],{"class":212},"\"blank not allowed\"",[202,1154,493],{"class":252},[202,1156,1157],{"class":204,"line":692},[202,1158,1159],{"class":248},"                        continue\n",[202,1161,1162,1164,1166,1168,1170,1173,1175,1178,1180],{"class":204,"line":713},[202,1163,1126],{"class":248},[202,1165,816],{"class":252},[202,1167,819],{"class":248},[202,1169,822],{"class":212},[202,1171,1172],{"class":248}," and",[202,1174,569],{"class":252},[202,1176,1177],{"class":248},"not",[202,1179,686],{"class":248},[202,1181,1182],{"class":252}," allowed:\n",[202,1184,1185,1188,1190,1193,1195,1198,1200,1202],{"class":204,"line":727},[202,1186,1187],{"class":252},"                        problems.append((cell.coordinate, v, ",[202,1189,329],{"class":248},[202,1191,1192],{"class":212},"\"not in ",[202,1194,336],{"class":335},[202,1196,1197],{"class":252},"allowed",[202,1199,345],{"class":335},[202,1201,332],{"class":212},[202,1203,493],{"class":252},[202,1205,1206,1208,1210,1212,1215,1217,1220,1222,1225],{"class":204,"line":740},[202,1207,1126],{"class":248},[202,1209,816],{"class":252},[202,1211,819],{"class":248},[202,1213,1214],{"class":212}," \"whole\"",[202,1216,1172],{"class":248},[202,1218,1219],{"class":252}," dv.operator ",[202,1221,819],{"class":248},[202,1223,1224],{"class":212}," \"between\"",[202,1226,825],{"class":252},[202,1228,1229,1232,1234,1237,1240,1243],{"class":204,"line":766},[202,1230,1231],{"class":252},"                        low, high ",[202,1233,275],{"class":248},[202,1235,1236],{"class":322}," int",[202,1238,1239],{"class":252},"(dv.formula1), ",[202,1241,1242],{"class":322},"int",[202,1244,1245],{"class":252},"(dv.formula2)\n",[202,1247,1248,1250,1252,1255,1258,1261,1263,1265,1267,1270,1273,1275,1277],{"class":204,"line":795},[202,1249,1139],{"class":248},[202,1251,583],{"class":248},[202,1253,1254],{"class":252}," (",[202,1256,1257],{"class":322},"isinstance",[202,1259,1260],{"class":252},"(v, ",[202,1262,1242],{"class":322},[202,1264,635],{"class":252},[202,1266,638],{"class":248},[202,1268,1269],{"class":252}," low ",[202,1271,1272],{"class":248},"\u003C=",[202,1274,569],{"class":252},[202,1276,1272],{"class":248},[202,1278,1279],{"class":252}," high):\n",[202,1281,1282,1284,1286,1289,1291,1294,1296,1299,1301,1304,1306,1308],{"class":204,"line":800},[202,1283,1149],{"class":252},[202,1285,329],{"class":248},[202,1287,1288],{"class":212},"\"not a whole number ",[202,1290,336],{"class":335},[202,1292,1293],{"class":252},"low",[202,1295,345],{"class":335},[202,1297,1298],{"class":212},"-",[202,1300,336],{"class":335},[202,1302,1303],{"class":252},"high",[202,1305,345],{"class":335},[202,1307,332],{"class":212},[202,1309,493],{"class":252},[202,1311,1312,1314],{"class":204,"line":811},[202,1313,551],{"class":248},[202,1315,1316],{"class":252}," problems\n",[202,1318,1319],{"class":204,"line":828},[202,1320,266],{"emptyLinePlaceholder":265},[202,1322,1324,1326,1329,1331],{"class":204,"line":1323},22,[202,1325,307],{"class":248},[202,1327,1328],{"class":252}," coord, value, reason ",[202,1330,313],{"class":248},[202,1332,1333],{"class":252}," check_rules(wb, ws):\n",[202,1335,1337,1339,1341,1343,1345,1347,1350,1353,1355,1358,1361,1364,1366,1368,1371,1373,1375],{"class":204,"line":1336},23,[202,1338,323],{"class":322},[202,1340,326],{"class":252},[202,1342,329],{"class":248},[202,1344,332],{"class":212},[202,1346,336],{"class":335},[202,1348,1349],{"class":252},"coord",[202,1351,1352],{"class":248},":5",[202,1354,345],{"class":335},[202,1356,1357],{"class":335}," {",[202,1359,1360],{"class":252},"value",[202,1362,1363],{"class":248},"!r:10",[202,1365,345],{"class":335},[202,1367,1357],{"class":335},[202,1369,1370],{"class":252},"reason",[202,1372,345],{"class":335},[202,1374,332],{"class":212},[202,1376,284],{"class":252},[10,1378,1379,1380,1383,1384,487,1387,1390,1391,1394,1395,1398],{},"The second data row breaks every rule — a region, status and priority that are not in their lists and a quantity of 900 — and the fourth has a quantity of 0. The blank region in row 5 is fine, because that rule allows blanks. Capping the loop at ",[199,1381,1382],{},"ws.max_row"," stops it from walking the hundred empty rows the template validates ahead of time. Extend the number check to the other operators (",[199,1385,1386],{},"greaterThan",[199,1388,1389],{},"lessThanOrEqual"," and so on) and to ",[199,1392,1393],{},"decimal"," and ",[199,1396,1397],{},"date"," rules as your templates need them.",[187,1400,1402],{"id":1401},"step-4-report-the-problems-where-people-will-fix-them","Step 4 — Report the problems where people will fix them",[10,1404,1405],{},"A list of coordinates in a log is easy to ignore. Highlight the offending cells in a copy of the workbook, with the reason as a comment, and send that back to whoever filled it in:",[192,1407,1409],{"className":239,"code":1408,"language":241,"meta":197,"style":197},"from openpyxl.comments import Comment\nfrom openpyxl.styles import PatternFill\n\nflag = PatternFill(\"solid\", fgColor=\"FFC7CE\")\nfor coord, value, reason in check_rules(wb, ws):\n    ws[coord].fill = flag\n    ws[coord].comment = Comment(reason, \"validation check\")\nwb.save(\"orders_checked.xlsx\")\nprint(sum(1 for c in load_workbook(\"orders_checked.xlsx\")[\"Orders\"][\"A2:D5\"] for cell in c if cell.comment), \"cells flagged\")\n",[199,1410,1411,1423,1435,1439,1464,1474,1484,1499,1509],{"__ignoreMap":197},[202,1412,1413,1415,1418,1420],{"class":204,"line":205},[202,1414,249],{"class":248},[202,1416,1417],{"class":252}," openpyxl.comments ",[202,1419,256],{"class":248},[202,1421,1422],{"class":252}," Comment\n",[202,1424,1425,1427,1430,1432],{"class":204,"line":262},[202,1426,249],{"class":248},[202,1428,1429],{"class":252}," openpyxl.styles ",[202,1431,256],{"class":248},[202,1433,1434],{"class":252}," PatternFill\n",[202,1436,1437],{"class":204,"line":269},[202,1438,266],{"emptyLinePlaceholder":265},[202,1440,1441,1444,1446,1449,1452,1454,1457,1459,1462],{"class":204,"line":287},[202,1442,1443],{"class":252},"flag ",[202,1445,275],{"class":248},[202,1447,1448],{"class":252}," PatternFill(",[202,1450,1451],{"class":212},"\"solid\"",[202,1453,487],{"class":252},[202,1455,1456],{"class":506},"fgColor",[202,1458,275],{"class":248},[202,1460,1461],{"class":212},"\"FFC7CE\"",[202,1463,284],{"class":252},[202,1465,1466,1468,1470,1472],{"class":204,"line":304},[202,1467,307],{"class":248},[202,1469,1328],{"class":252},[202,1471,313],{"class":248},[202,1473,1333],{"class":252},[202,1475,1476,1479,1481],{"class":204,"line":319},[202,1477,1478],{"class":252},"    ws[coord].fill ",[202,1480,275],{"class":248},[202,1482,1483],{"class":252}," flag\n",[202,1485,1486,1489,1491,1494,1497],{"class":204,"line":591},[202,1487,1488],{"class":252},"    ws[coord].comment ",[202,1490,275],{"class":248},[202,1492,1493],{"class":252}," Comment(reason, ",[202,1495,1496],{"class":212},"\"validation check\"",[202,1498,284],{"class":252},[202,1500,1501,1504,1507],{"class":204,"line":596},[202,1502,1503],{"class":252},"wb.save(",[202,1505,1506],{"class":212},"\"orders_checked.xlsx\"",[202,1508,284],{"class":252},[202,1510,1511,1514,1516,1519,1521,1523,1526,1529,1531,1533,1535,1538,1540,1543,1546,1549,1551,1553,1555,1557,1559,1562,1565],{"class":204,"line":607},[202,1512,1513],{"class":322},"print",[202,1515,326],{"class":252},[202,1517,1518],{"class":322},"sum",[202,1520,326],{"class":252},[202,1522,708],{"class":322},[202,1524,1525],{"class":248}," for",[202,1527,1528],{"class":252}," c ",[202,1530,313],{"class":248},[202,1532,278],{"class":252},[202,1534,1506],{"class":212},[202,1536,1537],{"class":252},")[",[202,1539,298],{"class":212},[202,1541,1542],{"class":252},"][",[202,1544,1545],{"class":212},"\"A2:D5\"",[202,1547,1548],{"class":252},"] ",[202,1550,307],{"class":248},[202,1552,1106],{"class":252},[202,1554,313],{"class":248},[202,1556,1528],{"class":252},[202,1558,576],{"class":248},[202,1560,1561],{"class":252}," cell.comment), ",[202,1563,1564],{"class":212},"\"cells flagged\"",[202,1566,284],{"class":252},[10,1568,1569,1570,1574],{},"Each flagged cell carries its own explanation, so the person fixing it does not need the script's output. ",[14,1571,1573],{"href":1572},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fhighlight-invalid-cells-in-excel-with-python\u002F","Highlight Invalid Cells in Excel with Python"," covers the highlighting options in more detail.",[20,1576,29,1582,29,1585,29,1588,29,1593,29,1599,29,1604,29,1608,29,1614,29,1618,29,1622,29,1626,29,1630,29,1634,29,1638,29,1642,29,1646,29,1648,29,1652,29,1654,29,1657,29,1659,29,1662,29,1664,29,1667,29,1670,29,1674,29,1676,29,1680,29,1682,29,1685,29,1687,29,1690,29,1692,29,1695,29,1698,29,1702,29,1707,29,1711,29,1713,29,1716,29,1718,29,1721,29,1723,29,1726,29,1729,29,1732,29,1734,29,1737,29,1739,29,1742,29,1744,29,1747,29,1749,29,1752,29,1755,29,1759,29,1761,29,1763,29,1766,29,1768,29,1771,29,1773],{"viewBox":1577,"role":23,"ariaLabelledBy":1578,"xmlns":27,"style":1581},"119 0 521 228",[1579,1580],"dv-flagged-t","dv-flagged-d","width:100%;max-width:521px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[31,1583,1584],{"id":1579},"Entered data checked against the template's own rules",[35,1586,1587],{"id":1580},"The Orders sheet after checking. Row 3 has West, late, Urgent and 900, all outside their rules, and every cell is flagged. Row 5 has a quantity of 0, below the minimum of 1, and is flagged. Row 5's blank region is allowed and is not flagged.",[39,1589],{"x":1590,"y":42,"width":1591,"height":1592,"fill":45},"119","521","228",[68,1594,1598],{"x":1595,"y":1596,"style":1597},"380.0","31.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","orders_checked.xlsx — flagged cells carry the reason as a comment",[39,1600],{"x":1601,"y":1602,"width":909,"height":1603,"rx":42,"fill":906,"stroke":882,"style":883},"239.4","48.0","24.0",[39,1605],{"x":1606,"y":1602,"width":1607,"height":1603,"rx":42,"fill":906,"stroke":882,"style":883},"271.4","63.6",[68,1609,1613],{"x":1610,"y":1611,"style":1612},"303.2","65.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[39,1615],{"x":1616,"y":1602,"width":1617,"height":1603,"rx":42,"fill":906,"stroke":882,"style":883},"335.0","62.5",[68,1619,1621],{"x":1620,"y":1611,"style":1612},"366.2","B",[39,1623],{"x":1624,"y":1602,"width":1625,"height":1603,"rx":42,"fill":906,"stroke":882,"style":883},"397.5","74.0",[68,1627,1629],{"x":1628,"y":1611,"style":1612},"434.5","C",[39,1631],{"x":1632,"y":1602,"width":1633,"height":1603,"rx":42,"fill":906,"stroke":882,"style":883},"471.4","49.2",[68,1635,1637],{"x":1636,"y":1611,"style":1612},"496.0","D",[39,1639],{"x":1601,"y":1640,"width":909,"height":1641,"rx":42,"fill":906,"stroke":882,"style":883},"72.0","28.0",[68,1643,708],{"x":1644,"y":1645,"style":1612},"255.4","91.0",[39,1647],{"x":1606,"y":1640,"width":1607,"height":1641,"rx":42,"fill":45,"stroke":882,"style":883},[68,1649,1651],{"x":1610,"y":1645,"style":1650},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","region",[39,1653],{"x":1616,"y":1640,"width":1617,"height":1641,"rx":42,"fill":45,"stroke":882,"style":883},[68,1655,1656],{"x":1620,"y":1645,"style":1650},"status",[39,1658],{"x":1624,"y":1640,"width":1625,"height":1641,"rx":42,"fill":45,"stroke":882,"style":883},[68,1660,1661],{"x":1628,"y":1645,"style":1650},"priority",[39,1663],{"x":1632,"y":1640,"width":1633,"height":1641,"rx":42,"fill":45,"stroke":882,"style":883},[68,1665,1666],{"x":1636,"y":1645,"style":1650},"qty",[39,1668],{"x":1601,"y":1669,"width":909,"height":1641,"rx":42,"fill":906,"stroke":882,"style":883},"100.0",[68,1671,1673],{"x":1644,"y":1672,"style":1612},"119.0","2",[39,1675],{"x":1606,"y":1669,"width":1607,"height":1641,"rx":42,"fill":45,"stroke":882,"style":883},[68,1677,1679],{"x":1610,"y":1672,"style":1678},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","North",[39,1681],{"x":1616,"y":1669,"width":1617,"height":1641,"rx":42,"fill":45,"stroke":882,"style":883},[68,1683,1684],{"x":1620,"y":1672,"style":1678},"paid",[39,1686],{"x":1624,"y":1669,"width":1625,"height":1641,"rx":42,"fill":45,"stroke":882,"style":883},[68,1688,1689],{"x":1628,"y":1672,"style":1678},"High",[39,1691],{"x":1632,"y":1669,"width":1633,"height":1641,"rx":42,"fill":45,"stroke":882,"style":883},[68,1693,1694],{"x":1636,"y":1672,"style":1678},"10",[39,1696],{"x":1601,"y":1697,"width":909,"height":1641,"rx":42,"fill":906,"stroke":882,"style":883},"128.0",[68,1699,1701],{"x":1644,"y":1700,"style":1612},"147.0","3",[39,1703],{"x":1606,"y":1697,"width":1607,"height":1641,"rx":42,"fill":1704,"stroke":1705,"style":1706},"#fee8f2","var(--accent,#f43f8f)","stroke-width:1.5px",[68,1708,1710],{"x":1610,"y":1700,"style":1709},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","West",[39,1712],{"x":1616,"y":1697,"width":1617,"height":1641,"rx":42,"fill":1704,"stroke":1705,"style":1706},[68,1714,1715],{"x":1620,"y":1700,"style":1709},"late",[39,1717],{"x":1624,"y":1697,"width":1625,"height":1641,"rx":42,"fill":1704,"stroke":1705,"style":1706},[68,1719,1720],{"x":1628,"y":1700,"style":1709},"Urgent",[39,1722],{"x":1632,"y":1697,"width":1633,"height":1641,"rx":42,"fill":1704,"stroke":1705,"style":1706},[68,1724,1725],{"x":1636,"y":1700,"style":1709},"900",[39,1727],{"x":1601,"y":1728,"width":909,"height":1641,"rx":42,"fill":906,"stroke":882,"style":883},"156.0",[68,1730,41],{"x":1644,"y":1731,"style":1612},"175.0",[39,1733],{"x":1606,"y":1728,"width":1607,"height":1641,"rx":42,"fill":45,"stroke":882,"style":883},[68,1735,1736],{"x":1610,"y":1731,"style":1678},"South",[39,1738],{"x":1616,"y":1728,"width":1617,"height":1641,"rx":42,"fill":45,"stroke":882,"style":883},[68,1740,1741],{"x":1620,"y":1731,"style":1678},"open",[39,1743],{"x":1624,"y":1728,"width":1625,"height":1641,"rx":42,"fill":45,"stroke":882,"style":883},[68,1745,1746],{"x":1628,"y":1731,"style":1678},"Low",[39,1748],{"x":1632,"y":1728,"width":1633,"height":1641,"rx":42,"fill":45,"stroke":882,"style":883},[68,1750,1751],{"x":1636,"y":1731,"style":1678},"25",[39,1753],{"x":1601,"y":1754,"width":909,"height":1641,"rx":42,"fill":906,"stroke":882,"style":883},"184.0",[68,1756,1758],{"x":1644,"y":1757,"style":1612},"203.0","5",[39,1760],{"x":1606,"y":1754,"width":1607,"height":1641,"rx":42,"fill":45,"stroke":882,"style":883},[39,1762],{"x":1616,"y":1754,"width":1617,"height":1641,"rx":42,"fill":45,"stroke":882,"style":883},[68,1764,1765],{"x":1620,"y":1757,"style":1678},"void",[39,1767],{"x":1624,"y":1754,"width":1625,"height":1641,"rx":42,"fill":45,"stroke":882,"style":883},[68,1769,1770],{"x":1628,"y":1757,"style":1678},"Medium",[39,1772],{"x":1632,"y":1754,"width":1633,"height":1641,"rx":42,"fill":1704,"stroke":1705,"style":1706},[68,1774,42],{"x":1636,"y":1757,"style":1709},[187,1776,1778],{"id":1777},"step-5-export-the-rules-as-documentation","Step 5 — Export the rules as documentation",[10,1780,1781],{},"The same resolution gives a compact description of a template's rules — useful for handing to a new team member or checking a template before it is distributed:",[192,1783,1785],{"className":239,"code":1784,"language":241,"meta":197,"style":197},"import pandas as pd\n\ndoc = []\nfor dv in ws.data_validations.dataValidation:\n    header = ws.cell(row=1, column=min(r.min_col for r in dv.sqref.ranges)).value\n    rule = \", \".join(map(str, list_values(wb, dv.formula1))) if dv.type == \"list\" else f\"{dv.type} {dv.operator} {dv.formula1}-{dv.formula2}\"\n    doc.append({\"column\": header, \"cells\": str(dv.sqref), \"rule\": rule, \"blank allowed\": bool(dv.allow_blank)})\nprint(pd.DataFrame(doc).to_string(index=False))\n",[199,1786,1787,1800,1804,1813,1823,1862,1930,1968],{"__ignoreMap":197},[202,1788,1789,1791,1794,1797],{"class":204,"line":205},[202,1790,256],{"class":248},[202,1792,1793],{"class":252}," pandas ",[202,1795,1796],{"class":248},"as",[202,1798,1799],{"class":252}," pd\n",[202,1801,1802],{"class":204,"line":262},[202,1803,266],{"emptyLinePlaceholder":265},[202,1805,1806,1809,1811],{"class":204,"line":269},[202,1807,1808],{"class":252},"doc ",[202,1810,275],{"class":248},[202,1812,1004],{"class":252},[202,1814,1815,1817,1819,1821],{"class":204,"line":287},[202,1816,307],{"class":248},[202,1818,310],{"class":252},[202,1820,313],{"class":248},[202,1822,316],{"class":252},[202,1824,1825,1828,1830,1833,1836,1838,1840,1842,1845,1847,1849,1852,1854,1857,1859],{"class":204,"line":304},[202,1826,1827],{"class":252},"    header ",[202,1829,275],{"class":248},[202,1831,1832],{"class":252}," ws.cell(",[202,1834,1835],{"class":506},"row",[202,1837,275],{"class":248},[202,1839,708],{"class":322},[202,1841,487],{"class":252},[202,1843,1844],{"class":506},"column",[202,1846,275],{"class":248},[202,1848,1078],{"class":322},[202,1850,1851],{"class":252},"(r.min_col ",[202,1853,307],{"class":248},[202,1855,1856],{"class":252}," r ",[202,1858,313],{"class":248},[202,1860,1861],{"class":252}," dv.sqref.ranges)).value\n",[202,1863,1864,1867,1869,1872,1875,1878,1880,1882,1885,1887,1889,1891,1893,1895,1898,1900,1902,1904,1906,1908,1911,1913,1915,1917,1919,1921,1923,1925,1927],{"class":204,"line":319},[202,1865,1866],{"class":252},"    rule ",[202,1868,275],{"class":248},[202,1870,1871],{"class":212}," \", \"",[202,1873,1874],{"class":252},".join(",[202,1876,1877],{"class":322},"map",[202,1879,326],{"class":252},[202,1881,353],{"class":322},[202,1883,1884],{"class":252},", list_values(wb, dv.formula1))) ",[202,1886,576],{"class":248},[202,1888,816],{"class":252},[202,1890,819],{"class":248},[202,1892,822],{"class":212},[202,1894,1036],{"class":248},[202,1896,1897],{"class":248}," f",[202,1899,332],{"class":212},[202,1901,336],{"class":335},[202,1903,339],{"class":252},[202,1905,345],{"class":335},[202,1907,1357],{"class":335},[202,1909,1910],{"class":252},"dv.operator",[202,1912,345],{"class":335},[202,1914,1357],{"class":335},[202,1916,369],{"class":252},[202,1918,345],{"class":335},[202,1920,1298],{"class":212},[202,1922,336],{"class":335},[202,1924,382],{"class":252},[202,1926,345],{"class":335},[202,1928,1929],{"class":212},"\"\n",[202,1931,1932,1935,1938,1941,1944,1947,1949,1951,1954,1957,1960,1962,1965],{"class":204,"line":591},[202,1933,1934],{"class":252},"    doc.append({",[202,1936,1937],{"class":212},"\"column\"",[202,1939,1940],{"class":252},": header, ",[202,1942,1943],{"class":212},"\"cells\"",[202,1945,1946],{"class":252},": ",[202,1948,353],{"class":322},[202,1950,838],{"class":252},[202,1952,1953],{"class":212},"\"rule\"",[202,1955,1956],{"class":252},": rule, ",[202,1958,1959],{"class":212},"\"blank allowed\"",[202,1961,1946],{"class":252},[202,1963,1964],{"class":322},"bool",[202,1966,1967],{"class":252},"(dv.allow_blank)})\n",[202,1969,1970,1972,1975,1978,1980,1983],{"class":204,"line":596},[202,1971,1513],{"class":322},[202,1973,1974],{"class":252},"(pd.DataFrame(doc).to_string(",[202,1976,1977],{"class":506},"index",[202,1979,275],{"class":248},[202,1981,1982],{"class":322},"False",[202,1984,493],{"class":252},[187,1986,1988],{"id":1987},"rules-openpyxl-cannot-see","Rules openpyxl cannot see",[10,1990,1991,1992,1995,1996,1999,2000,2003,2004,2008],{},"Newer Excel versions store some validation rules — notably lists whose source is on another sheet, in files saved by certain versions — in an ",[432,1993,1994],{},"extension list"," that openpyxl does not read. It warns ",[199,1997,1998],{},"Data Validation extension is not supported and will be removed"," and presents the sheet without those rules; saving the workbook with openpyxl then deletes them. If a template's dropdowns are missing from ",[199,2001,2002],{},"data_validations",", that warning is the reason. ",[14,2005,2007],{"href":2006},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-data-validation-extension-is-not-supported-warning\u002F","Fix the \"Data Validation extension is not supported\" Warning in openpyxl"," explains how to read those rules another way and how to avoid losing them.",[187,2010,2012],{"id":2011},"common-pitfalls","Common pitfalls",[2014,2015,2016,2024,2035,2046,2052],"ul",{},[2017,2018,2019,2023],"li",{},[2020,2021,2022],"strong",{},"Hard-coding the allowed values."," Resolve them from the template so edits to the lists take effect.",[2017,2025,2026,2032,2033,2031],{},[2020,2027,2028,2029,2031],{},"Walking the whole ",[199,2030,407],{},"."," Templates validate far more rows than contain data; stop at ",[199,2034,515],{},[2017,2036,2037,2040,2041,1394,2043,2045],{},[2020,2038,2039],{},"Formula sources."," ",[199,2042,857],{},[199,2044,861],{}," lists need calculation; report them instead of guessing.",[2017,2047,2048,2051],{},[2020,2049,2050],{},"Assuming Excel enforced the rule."," Pasted and imported values bypass dropdowns; check them.",[2017,2053,2054,2057],{},[2020,2055,2056],{},"Saving the template with openpyxl."," Extension-list rules are dropped; write results to a copy.",[187,2059,2061],{"id":2060},"performance-notes","Performance notes",[10,2063,2064,2065,2068],{},"Reading rules is instant; checking visits each validated cell once. For large sheets, load the data with pandas and compare whole columns against the allowed lists with ",[199,2066,2067],{},"isin",", using openpyxl only to read and resolve the rules.",[187,2070,2072],{"id":2071},"conclusion","Conclusion",[10,2074,2075,2076,2078,2079,2081],{},"Read a workbook's dropdowns from the worksheet's ",[199,2077,235],{},", resolve each list rule's ",[199,2080,411],{}," — inline list, range reference or defined name — into its real values, and compare the entered data against every rule, including number limits and whether blanks are allowed. Flag violations in a copy of the workbook with the reason attached, export the rules as documentation, and remember that openpyxl cannot see rules stored in Excel's extension lists.",[187,2083,2085],{"id":2084},"frequently-asked-questions","Frequently asked questions",[10,2087,2088,2091],{},[2020,2089,2090],{},"Where does openpyxl keep a sheet's dropdowns?","\nIn the worksheet's data_validations.dataValidation, a list of rules. Each rule has a type (\"list\", \"whole\", \"decimal\", \"date\" and others), formula1 and formula2 holding its source or limits, and sqref, the cells it applies to.",[10,2093,2094,2097],{},[2020,2095,2096],{},"How do I get the actual values of a dropdown?","\nLook at formula1. A quoted comma-separated string is the list itself. A range such as Lists!$A$1:$A$3 must be read from that sheet. A single name refers to a defined name, whose destinations give the range to read.",[10,2099,2100,2103],{},[2020,2101,2102],{},"Why are some of the workbook's dropdowns missing?","\nopenpyxl does not read validation rules stored in Excel's extension lists, which newer Excel versions use for some rules, and warns \"Data Validation extension is not supported and will be removed\". Those rules are invisible to openpyxl and lost if the file is saved.",[10,2105,2106,2109],{},[2020,2107,2108],{},"Can this tell me which cells break their rule?","\nYes. Once each rule is resolved to its allowed values or limits, iterate over the cells in its sqref and compare. Excel only validates values as they are typed, so pasted or imported data often breaks the rules without anyone noticing.",[187,2111,2113],{"id":2112},"related","Related",[2014,2115,2116,2122,2129,2136],{},[2017,2117,2118,2119,2121],{},"Up one level: ",[14,2120,17],{"href":16}," — reading sheets built for people.",[2017,2123,2124,2128],{},[14,2125,2127],{"href":2126},"\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"," — creating the rules in the first place.",[2017,2130,2131,2135],{},[14,2132,2134],{"href":2133},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-columns-before-import-with-pandas\u002F","Validate Excel Columns Before Import with pandas"," — column checks in pandas.",[2017,2137,2138,2142],{},[14,2139,2141],{"href":2140},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fcreate-named-range-in-excel-with-openpyxl\u002F","Create a Named Range in Excel with openpyxl"," — the defined names list rules often use.",[2144,2145,2146],"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 .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}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}",{"title":197,"searchDepth":262,"depth":262,"links":2148},[2149,2150,2151,2152,2153,2154,2155,2156,2157,2158,2159,2160],{"id":189,"depth":262,"text":190},{"id":228,"depth":262,"text":229},{"id":423,"depth":262,"text":424},{"id":975,"depth":262,"text":976},{"id":1401,"depth":262,"text":1402},{"id":1777,"depth":262,"text":1778},{"id":1987,"depth":262,"text":1988},{"id":2011,"depth":262,"text":2012},{"id":2060,"depth":262,"text":2061},{"id":2071,"depth":262,"text":2072},{"id":2084,"depth":262,"text":2085},{"id":2112,"depth":262,"text":2113},"2026-09-25","Find out what a template's dropdowns allow — the regions, statuses and priority codes behind each data validation rule — and check the data people entered against them. Read validation rules with openpyxl, resolve inline lists, range references and named ranges to real values, and report every cell that breaks its rule.","md",[2165,2167,2169,2171],{"q":2090,"a":2166},"In the worksheet's data_validations.dataValidation, a list of rules. Each rule has a type (\"list\", \"whole\", \"decimal\", \"date\" and others), formula1 and formula2 holding its source or limits, and sqref, the cells it applies to.",{"q":2096,"a":2168},"Look at formula1. A quoted comma-separated string is the list itself. A range such as Lists!$A$1:$A$3 must be read from that sheet. A single name refers to a defined name, whose destinations give the range to read.",{"q":2102,"a":2170},"openpyxl does not read validation rules stored in Excel's extension lists, which newer Excel versions use for some rules, and warns \"Data Validation extension is not supported and will be removed\". Those rules are invisible to openpyxl and lost if the file is saved.",{"q":2108,"a":2172},"Yes. Once each rule is resolved to its allowed values or limits, iterate over the cells in its sqref and compare. Excel only validates values as they are typed, so pasted or imported data often breaks the rules without anyone noticing.","read data validation dropdown lists from an existing workbook",null,{"breadcrumb":2176},[2177,2180,2183],{"name":2178,"item":2179},"Home","\u002F",{"name":2181,"item":2182},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":17,"item":16},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-dropdown-lists-from-an-existing-workbook",{"title":2186,"description":2187},"Read Excel Data Validation Dropdown Lists with openpyxl","Extract dropdown list values and validation rules from an Excel workbook with openpyxl: data_validations, inline lists, list ranges on other sheets, named ranges, whole-number rules, and checking entered data against them.","read-dropdown-lists-from-an-existing-workbook","advanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-dropdown-lists-from-an-existing-workbook\u002Findex",[2191,2192,2193],"messy-sheets","data-validation","openpyxl","how-to","32ihggOye-RUxipraxgCoUAfrHmqh-ObHUdfhd9o4c0",[2197,2201],{"title":2198,"path":2199,"stem":2200,"children":-1},"Read Cell Colours and Font Styles as Data with openpyxl","\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-cell-colours-and-font-styles-as-data-with-openpyxl","advanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-cell-colours-and-font-styles-as-data-with-openpyxl\u002Findex",{"title":2202,"path":2203,"stem":2204,"children":-1},"Read Excel with Multi-Row Headers in pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-excel-with-multi-row-headers-in-pandas","advanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-excel-with-multi-row-headers-in-pandas\u002Findex",[2206,2208],{"path":2207,"title":17},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets",{"path":2209,"title":2181},"\u002Fadvanced-data-transformation-and-cleaning",[],[2212,2215,2218,2221,2224,2227],{"path":2213,"title":2214,"kind":2194},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Ffreeze-header-row-in-excel-with-openpyxl","Freeze the Header Row in Excel with openpyxl",{"path":2216,"title":2217,"kind":2194},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fhandle-merged-cells-when-reading-excel-with-pandas","Handle Merged Cells When Reading Excel with pandas",{"path":2219,"title":2220,"kind":2194},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Funmerge-cells-and-fill-values-with-openpyxl","Unmerge Cells and Fill the Values with openpyxl",{"path":2222,"title":2223,"kind":2194},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fcombine-sheets-with-different-layouts-into-one-table","Combine Sheets with Different Layouts into One Table",{"path":2225,"title":2226,"kind":2194},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fextract-hyperlink-urls-from-excel-cells","Extract Hyperlink URLs from Excel Cells with openpyxl",{"path":2228,"title":2229,"kind":2194},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fextract-report-header-fields-from-an-excel-sheet","Extract Report Header Fields from an Excel Sheet with Python",1790972091031]