[{"data":1,"prerenderedAt":2394},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fadd-dropdown-data-validation-to-excel-with-openpyxl":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fadd-dropdown-data-validation-to-excel-with-openpyxl":2386},{"id":4,"title":5,"body":6,"dateModified":2356,"datePublished":2356,"description":2357,"extension":2358,"faq":2359,"meta":2368,"navigation":235,"path":2379,"seo":2380,"slug":2382,"stem":2383,"type":2384,"__hash__":2385},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fadd-dropdown-data-validation-to-excel-with-openpyxl\u002Findex.md","Add Dropdown Data Validation to Excel with openpyxl",{"type":7,"value":8,"toc":2341},"minimark",[9,32,154,159,183,187,194,403,418,425,429,432,647,671,765,769,772,1114,1121,1125,1133,1440,1444,1454,1857,1890,1894,1897,2006,2009,2013,2016,2108,2111,2115,2220,2224,2235,2239,2242,2246,2256,2268,2280,2289,2293,2296,2305,2308,2337],[10,11,12,13,17,18,21,22,25,26,31],"p",{},"A dropdown is the cheapest data-quality control there is: the reader picks from a list instead of typing ",[14,15,16],"code",{},"Nrth",". ",[14,19,20],{},"openpyxl"," writes them through ",[14,23,24],{},"DataValidation",", and the whole feature is three lines — plus a handful of details about quoting, list length and where the source values live. This guide, part of ",[27,28,30],"a",{"href":29},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002F","Validating Excel Data with Python",", covers all of them.",[33,34,42,43,42,47,42,51,42,58,42,68,42,75,42,80,42,84,42,89,42,96,42,101,42,107,42,111,42,114,42,119,42,123,42,127,42,130,42,135,42,140,42,143,42,146,42,149],"svg",{"viewBox":35,"role":36,"ariaLabelledBy":37,"xmlns":40,"style":41},"0 0 740 220","img",[38,39],"dv-parts-t","dv-parts-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  ",[44,45,46],"title",{"id":38},"The three moving parts of a dropdown rule",[48,49,50],"desc",{"id":39},"A DataValidation object holds the type and the source list, the worksheet registers it with add_data_validation, and the rule is applied to one or more cell ranges with dv.add. All three steps are required for the dropdown to appear.",[52,53],"rect",{"x":54,"y":54,"width":55,"height":56,"fill":57},"0","740","220","#ffffff",[52,59],{"x":60,"y":61,"width":62,"height":63,"rx":64,"fill":65,"stroke":66,"style":67},"20","40","210","120","14","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[69,70,74],"text",{"x":71,"y":72,"style":73},"125","70","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","1 · describe the rule",[69,76,79],{"x":71,"y":77,"style":78},"96","font-size:11.5px;fill:var(--muted,#5b6780);text-anchor:middle","DataValidation(type=\"list\",",[69,81,83],{"x":71,"y":82,"style":78},"114","formula1=…)",[69,85,88],{"x":71,"y":86,"style":87},"140","font-size:11.5px;fill:var(--text,#172033);text-anchor:middle","values, messages, blanks",[90,91],"line",{"x1":92,"y1":93,"x2":94,"y2":93,"stroke":95,"style":67},"234","100","264","var(--muted,#5b6780)",[97,98],"polygon",{"points":99,"fill":100},"268,100 256,94 256,106","#5b6780",[52,102],{"x":103,"y":61,"width":104,"height":63,"rx":64,"fill":105,"stroke":106},"270","200","#ebebfd","var(--line,#cdd5e6)",[69,108,110],{"x":109,"y":72,"style":73},"370","2 · register it",[69,112,113],{"x":109,"y":77,"style":78},"ws.add_data_validation(dv)",[69,115,118],{"x":109,"y":116,"style":117},"126","font-size:11.5px;fill:var(--danger,#dc2626);text-anchor:middle","skip this and nothing",[69,120,122],{"x":109,"y":121,"style":117},"144","is written to the file",[90,124],{"x1":125,"y1":93,"x2":126,"y2":93,"stroke":95,"style":67},"474","504",[97,128],{"points":129,"fill":100},"508,100 496,94 496,106",[52,131],{"x":132,"y":61,"width":62,"height":63,"rx":64,"fill":133,"stroke":134,"style":67},"510","#d9f4f1","var(--teal,#0f9488)",[69,136,139],{"x":137,"y":72,"style":138},"615","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","3 · apply to ranges",[69,141,142],{"x":137,"y":77,"style":78},"dv.add(\"B2:B500\")",[69,144,145],{"x":137,"y":116,"style":87},"one rule can cover",[69,147,148],{"x":137,"y":121,"style":87},"many ranges",[69,150,153],{"x":109,"y":151,"style":152},"196","font-size:12px;fill:var(--muted,#5b6780);text-anchor:middle","The order matters: register the rule on the sheet, then attach ranges to it.",[155,156,158],"h2",{"id":157},"prerequisites","Prerequisites",[160,161,166],"pre",{"className":162,"code":163,"language":164,"meta":165,"style":165},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl\n","bash","",[14,167,168],{"__ignoreMap":165},[169,170,172,176,180],"span",{"class":90,"line":171},1,[169,173,175],{"class":174},"sMTad","pip",[169,177,179],{"class":178},"srMev"," install",[169,181,182],{"class":178}," openpyxl\n",[155,184,186],{"id":185},"the-minimal-dropdown","The minimal dropdown",[10,188,189,190,193],{},"An inline list is a quoted, comma-separated string. Note the quoting carefully — the value of ",[14,191,192],{},"formula1"," must itself contain double quotes:",[160,195,199],{"className":196,"code":197,"language":198,"meta":165,"style":165},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import Workbook\nfrom openpyxl.worksheet.datavalidation import DataValidation\n\nwb = Workbook()\nws = wb.active\nws.title = \"Orders\"\nws.append([\"Order_ID\", \"Region\", \"Quantity\"])\n\nregions = DataValidation(\n    type=\"list\",\n    formula1='\"North,South,West,Central\"',\n    allow_blank=True,\n    showErrorMessage=True,\n)\nws.add_data_validation(regions)\nregions.add(\"B2:B500\")\n\nwb.save(\"dropdown.xlsx\")\n","python",[14,200,201,217,230,237,249,260,271,294,299,310,325,338,352,364,370,376,387,392],{"__ignoreMap":165},[169,202,203,207,211,214],{"class":90,"line":171},[169,204,206],{"class":205},"s-kum","from",[169,208,210],{"class":209},"skGVy"," openpyxl ",[169,212,213],{"class":205},"import",[169,215,216],{"class":209}," Workbook\n",[169,218,220,222,225,227],{"class":90,"line":219},2,[169,221,206],{"class":205},[169,223,224],{"class":209}," openpyxl.worksheet.datavalidation ",[169,226,213],{"class":205},[169,228,229],{"class":209}," DataValidation\n",[169,231,233],{"class":90,"line":232},3,[169,234,236],{"emptyLinePlaceholder":235},true,"\n",[169,238,240,243,246],{"class":90,"line":239},4,[169,241,242],{"class":209},"wb ",[169,244,245],{"class":205},"=",[169,247,248],{"class":209}," Workbook()\n",[169,250,252,255,257],{"class":90,"line":251},5,[169,253,254],{"class":209},"ws ",[169,256,245],{"class":205},[169,258,259],{"class":209}," wb.active\n",[169,261,263,266,268],{"class":90,"line":262},6,[169,264,265],{"class":209},"ws.title ",[169,267,245],{"class":205},[169,269,270],{"class":178}," \"Orders\"\n",[169,272,274,277,280,283,286,288,291],{"class":90,"line":273},7,[169,275,276],{"class":209},"ws.append([",[169,278,279],{"class":178},"\"Order_ID\"",[169,281,282],{"class":209},", ",[169,284,285],{"class":178},"\"Region\"",[169,287,282],{"class":209},[169,289,290],{"class":178},"\"Quantity\"",[169,292,293],{"class":209},"])\n",[169,295,297],{"class":90,"line":296},8,[169,298,236],{"emptyLinePlaceholder":235},[169,300,302,305,307],{"class":90,"line":301},9,[169,303,304],{"class":209},"regions ",[169,306,245],{"class":205},[169,308,309],{"class":209}," DataValidation(\n",[169,311,313,317,319,322],{"class":90,"line":312},10,[169,314,316],{"class":315},"sa561","    type",[169,318,245],{"class":205},[169,320,321],{"class":178},"\"list\"",[169,323,324],{"class":209},",\n",[169,326,328,331,333,336],{"class":90,"line":327},11,[169,329,330],{"class":315},"    formula1",[169,332,245],{"class":205},[169,334,335],{"class":178},"'\"North,South,West,Central\"'",[169,337,324],{"class":209},[169,339,341,344,346,350],{"class":90,"line":340},12,[169,342,343],{"class":315},"    allow_blank",[169,345,245],{"class":205},[169,347,349],{"class":348},"sP0c6","True",[169,351,324],{"class":209},[169,353,355,358,360,362],{"class":90,"line":354},13,[169,356,357],{"class":315},"    showErrorMessage",[169,359,245],{"class":205},[169,361,349],{"class":348},[169,363,324],{"class":209},[169,365,367],{"class":90,"line":366},14,[169,368,369],{"class":209},")\n",[169,371,373],{"class":90,"line":372},15,[169,374,375],{"class":209},"ws.add_data_validation(regions)\n",[169,377,379,382,385],{"class":90,"line":378},16,[169,380,381],{"class":209},"regions.add(",[169,383,384],{"class":178},"\"B2:B500\"",[169,386,369],{"class":209},[169,388,390],{"class":90,"line":389},17,[169,391,236],{"emptyLinePlaceholder":235},[169,393,395,398,401],{"class":90,"line":394},18,[169,396,397],{"class":209},"wb.save(",[169,399,400],{"class":178},"\"dropdown.xlsx\"",[169,402,369],{"class":209},[10,404,405,406,409,410,413,414,417],{},"Open the file and any cell from ",[14,407,408],{},"B2"," to ",[14,411,412],{},"B500"," now shows a dropdown arrow. ",[14,415,416],{},"allow_blank=True"," lets a user clear a cell without a warning, which is nearly always what you want on a form people fill in gradually.",[10,419,420,421,424],{},"The three-step order is worth internalising: build the rule, add it to the worksheet, then attach ranges. Calling ",[14,422,423],{},"dv.add(...)"," on a rule that was never registered writes nothing to the file and produces no error — the commonest reason a dropdown \"does not appear\".",[155,426,428],{"id":427},"add-messages-that-explain-the-rule","Add messages that explain the rule",[10,430,431],{},"An error box saying \"The value doesn't match the data validation restrictions\" teaches nobody anything. Set your own:",[160,433,435],{"className":196,"code":434,"language":198,"meta":165,"style":165},"from openpyxl import Workbook\nfrom openpyxl.worksheet.datavalidation import DataValidation\n\nwb = Workbook()\nws = wb.active\nws.append([\"Order_ID\", \"Region\"])\n\ndv = DataValidation(\n    type=\"list\",\n    formula1='\"North,South,West,Central\"',\n    allow_blank=False,\n    showErrorMessage=True,\n    showInputMessage=True,\n    errorStyle=\"stop\",          # \"stop\", \"warning\" or \"information\"\n)\ndv.errorTitle = \"Region not recognised\"\ndv.error = \"Pick one of the four sales regions from the dropdown. Ask ops if a new region is missing.\"\ndv.promptTitle = \"Sales region\"\ndv.prompt = \"Choose the region the order was booked in.\"\n\nws.add_data_validation(dv)\ndv.add(\"B2:B500\")\nwb.save(\"dropdown_messages.xlsx\")\n",[14,436,437,447,457,461,469,477,489,493,502,512,522,533,543,554,571,575,585,595,605,616,621,627,637],{"__ignoreMap":165},[169,438,439,441,443,445],{"class":90,"line":171},[169,440,206],{"class":205},[169,442,210],{"class":209},[169,444,213],{"class":205},[169,446,216],{"class":209},[169,448,449,451,453,455],{"class":90,"line":219},[169,450,206],{"class":205},[169,452,224],{"class":209},[169,454,213],{"class":205},[169,456,229],{"class":209},[169,458,459],{"class":90,"line":232},[169,460,236],{"emptyLinePlaceholder":235},[169,462,463,465,467],{"class":90,"line":239},[169,464,242],{"class":209},[169,466,245],{"class":205},[169,468,248],{"class":209},[169,470,471,473,475],{"class":90,"line":251},[169,472,254],{"class":209},[169,474,245],{"class":205},[169,476,259],{"class":209},[169,478,479,481,483,485,487],{"class":90,"line":262},[169,480,276],{"class":209},[169,482,279],{"class":178},[169,484,282],{"class":209},[169,486,285],{"class":178},[169,488,293],{"class":209},[169,490,491],{"class":90,"line":273},[169,492,236],{"emptyLinePlaceholder":235},[169,494,495,498,500],{"class":90,"line":296},[169,496,497],{"class":209},"dv ",[169,499,245],{"class":205},[169,501,309],{"class":209},[169,503,504,506,508,510],{"class":90,"line":301},[169,505,316],{"class":315},[169,507,245],{"class":205},[169,509,321],{"class":178},[169,511,324],{"class":209},[169,513,514,516,518,520],{"class":90,"line":312},[169,515,330],{"class":315},[169,517,245],{"class":205},[169,519,335],{"class":178},[169,521,324],{"class":209},[169,523,524,526,528,531],{"class":90,"line":327},[169,525,343],{"class":315},[169,527,245],{"class":205},[169,529,530],{"class":348},"False",[169,532,324],{"class":209},[169,534,535,537,539,541],{"class":90,"line":340},[169,536,357],{"class":315},[169,538,245],{"class":205},[169,540,349],{"class":348},[169,542,324],{"class":209},[169,544,545,548,550,552],{"class":90,"line":354},[169,546,547],{"class":315},"    showInputMessage",[169,549,245],{"class":205},[169,551,349],{"class":348},[169,553,324],{"class":209},[169,555,556,559,561,564,567],{"class":90,"line":366},[169,557,558],{"class":315},"    errorStyle",[169,560,245],{"class":205},[169,562,563],{"class":178},"\"stop\"",[169,565,566],{"class":209},",          ",[169,568,570],{"class":569},"s-wDw","# \"stop\", \"warning\" or \"information\"\n",[169,572,573],{"class":90,"line":372},[169,574,369],{"class":209},[169,576,577,580,582],{"class":90,"line":378},[169,578,579],{"class":209},"dv.errorTitle ",[169,581,245],{"class":205},[169,583,584],{"class":178}," \"Region not recognised\"\n",[169,586,587,590,592],{"class":90,"line":389},[169,588,589],{"class":209},"dv.error ",[169,591,245],{"class":205},[169,593,594],{"class":178}," \"Pick one of the four sales regions from the dropdown. Ask ops if a new region is missing.\"\n",[169,596,597,600,602],{"class":90,"line":394},[169,598,599],{"class":209},"dv.promptTitle ",[169,601,245],{"class":205},[169,603,604],{"class":178}," \"Sales region\"\n",[169,606,608,611,613],{"class":90,"line":607},19,[169,609,610],{"class":209},"dv.prompt ",[169,612,245],{"class":205},[169,614,615],{"class":178}," \"Choose the region the order was booked in.\"\n",[169,617,619],{"class":90,"line":618},20,[169,620,236],{"emptyLinePlaceholder":235},[169,622,624],{"class":90,"line":623},21,[169,625,626],{"class":209},"ws.add_data_validation(dv)\n",[169,628,630,633,635],{"class":90,"line":629},22,[169,631,632],{"class":209},"dv.add(",[169,634,384],{"class":178},[169,636,369],{"class":209},[169,638,640,642,645],{"class":90,"line":639},23,[169,641,397],{"class":209},[169,643,644],{"class":178},"\"dropdown_messages.xlsx\"",[169,646,369],{"class":209},[10,648,649,652,653,656,657,660,661,664,665,667,668,670],{},[14,650,651],{},"errorStyle"," decides how firm the rule is. ",[14,654,655],{},"stop"," refuses the value outright; ",[14,658,659],{},"warning"," lets the user override after confirming; ",[14,662,663],{},"information"," merely notes it. For a field your pipeline will reject anyway, ",[14,666,655],{}," is honest. For a judgement call — an unusually large quantity, say — ",[14,669,659],{}," respects that the user may know something your rule does not.",[33,672,42,677,42,680,42,683,42,686,42,692,42,697,42,701,42,705,42,712,42,717,42,720,42,725,42,728,42,731,42,734,42,737,42,740,42,743,42,746,42,750,42,753,42,756,42,759,42,762],{"viewBox":673,"role":36,"ariaLabelledBy":674,"xmlns":40,"style":41},"0 0 740 214",[675,676],"dv-style-t","dv-style-d",[44,678,679],{"id":675},"What each error style does when a value breaks the rule",[48,681,682],{"id":676},"Stop refuses the entry entirely. Warning asks the user to confirm and lets the value through if they insist. Information tells the user and accepts the value. Choose stop for values your pipeline will reject anyway.",[52,684],{"x":54,"y":54,"width":55,"height":685,"fill":57},"214",[52,687],{"x":60,"y":688,"width":56,"height":689,"rx":64,"fill":690,"stroke":691,"style":67},"30","160","#fce9e9","var(--danger,#dc2626)",[69,693,655],{"x":694,"y":695,"style":696},"130","58","font-size:13.5px;font-weight:700;fill:var(--danger,#dc2626);text-anchor:middle",[69,698,700],{"x":694,"y":699,"style":78},"86","value refused",[69,702,704],{"x":694,"y":703,"style":78},"106","cell keeps its old content",[52,706],{"x":707,"y":708,"width":709,"height":710,"rx":711,"fill":57,"stroke":106},"42","122","176","46","8",[69,713,716],{"x":694,"y":714,"style":715},"142","font-size:11.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","use for values your",[69,718,719],{"x":694,"y":689,"style":715},"job would reject",[52,721],{"x":722,"y":688,"width":56,"height":689,"rx":64,"fill":723,"stroke":724,"style":67},"260","#fdefd8","var(--gold,#b4740a)",[69,726,659],{"x":109,"y":695,"style":727},"font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",[69,729,730],{"x":109,"y":699,"style":78},"\"are you sure?\"",[69,732,733],{"x":109,"y":703,"style":78},"accepted if confirmed",[52,735],{"x":736,"y":708,"width":709,"height":710,"rx":711,"fill":57,"stroke":106},"282",[69,738,739],{"x":109,"y":714,"style":715},"use for unusual but",[69,741,742],{"x":109,"y":689,"style":715},"possible values",[52,744],{"x":745,"y":688,"width":56,"height":689,"rx":64,"fill":133,"stroke":134,"style":67},"500",[69,747,663],{"x":748,"y":695,"style":749},"610","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[69,751,752],{"x":748,"y":699,"style":78},"note shown",[69,754,755],{"x":748,"y":703,"style":78},"value always accepted",[52,757],{"x":758,"y":708,"width":709,"height":710,"rx":711,"fill":57,"stroke":106},"522",[69,760,761],{"x":748,"y":714,"style":715},"use for guidance,",[69,763,764],{"x":748,"y":689,"style":715},"not enforcement",[155,766,768],{"id":767},"lists-longer-than-255-characters","Lists longer than 255 characters",[10,770,771],{},"An inline list is stored as one formula string, and Excel caps that at 255 characters. Somewhere around twenty-five region names the dropdown silently stops working. Put the values on a sheet instead:",[160,773,775],{"className":196,"code":774,"language":198,"meta":165,"style":165},"from openpyxl import Workbook\nfrom openpyxl.worksheet.datavalidation import DataValidation\n\nproducts = [f\"SKU-{n:04d}\" for n in range(1, 121)]      # far beyond 255 characters\n\nwb = Workbook()\norders = wb.active\norders.title = \"Orders\"\norders.append([\"Order_ID\", \"SKU\"])\n\nlookups = wb.create_sheet(\"Lookups\")\nlookups[\"A1\"] = \"SKU\"\nfor i, sku in enumerate(products, start=2):\n    lookups[f\"A{i}\"] = sku\nlookups.sheet_state = \"hidden\"        # out of the way, still readable by the rule\n\ndv = DataValidation(\n    type=\"list\",\n    formula1=f\"'Lookups'!$A$2:$A${len(products) + 1}\",\n    allow_blank=True,\n    showErrorMessage=True,\n)\norders.add_data_validation(dv)\ndv.add(\"B2:B1000\")\n\nwb.save(\"dropdown_lookup.xlsx\")\n",[14,776,777,787,797,801,862,866,874,883,892,906,910,925,941,968,994,1007,1011,1019,1029,1060,1070,1080,1084,1089,1099,1104],{"__ignoreMap":165},[169,778,779,781,783,785],{"class":90,"line":171},[169,780,206],{"class":205},[169,782,210],{"class":209},[169,784,213],{"class":205},[169,786,216],{"class":209},[169,788,789,791,793,795],{"class":90,"line":219},[169,790,206],{"class":205},[169,792,224],{"class":209},[169,794,213],{"class":205},[169,796,229],{"class":209},[169,798,799],{"class":90,"line":232},[169,800,236],{"emptyLinePlaceholder":235},[169,802,803,806,808,811,814,817,821,824,827,830,833,836,839,842,845,848,851,853,856,859],{"class":90,"line":239},[169,804,805],{"class":209},"products ",[169,807,245],{"class":205},[169,809,810],{"class":209}," [",[169,812,813],{"class":205},"f",[169,815,816],{"class":178},"\"SKU-",[169,818,820],{"class":819},"sSjpA","{",[169,822,823],{"class":209},"n",[169,825,826],{"class":205},":04d",[169,828,829],{"class":819},"}",[169,831,832],{"class":178},"\"",[169,834,835],{"class":205}," for",[169,837,838],{"class":209}," n ",[169,840,841],{"class":205},"in",[169,843,844],{"class":348}," range",[169,846,847],{"class":209},"(",[169,849,850],{"class":348},"1",[169,852,282],{"class":209},[169,854,855],{"class":348},"121",[169,857,858],{"class":209},")]      ",[169,860,861],{"class":569},"# far beyond 255 characters\n",[169,863,864],{"class":90,"line":251},[169,865,236],{"emptyLinePlaceholder":235},[169,867,868,870,872],{"class":90,"line":262},[169,869,242],{"class":209},[169,871,245],{"class":205},[169,873,248],{"class":209},[169,875,876,879,881],{"class":90,"line":273},[169,877,878],{"class":209},"orders ",[169,880,245],{"class":205},[169,882,259],{"class":209},[169,884,885,888,890],{"class":90,"line":296},[169,886,887],{"class":209},"orders.title ",[169,889,245],{"class":205},[169,891,270],{"class":178},[169,893,894,897,899,901,904],{"class":90,"line":301},[169,895,896],{"class":209},"orders.append([",[169,898,279],{"class":178},[169,900,282],{"class":209},[169,902,903],{"class":178},"\"SKU\"",[169,905,293],{"class":209},[169,907,908],{"class":90,"line":312},[169,909,236],{"emptyLinePlaceholder":235},[169,911,912,915,917,920,923],{"class":90,"line":327},[169,913,914],{"class":209},"lookups ",[169,916,245],{"class":205},[169,918,919],{"class":209}," wb.create_sheet(",[169,921,922],{"class":178},"\"Lookups\"",[169,924,369],{"class":209},[169,926,927,930,933,936,938],{"class":90,"line":340},[169,928,929],{"class":209},"lookups[",[169,931,932],{"class":178},"\"A1\"",[169,934,935],{"class":209},"] ",[169,937,245],{"class":205},[169,939,940],{"class":178}," \"SKU\"\n",[169,942,943,946,949,951,954,957,960,962,965],{"class":90,"line":354},[169,944,945],{"class":205},"for",[169,947,948],{"class":209}," i, sku ",[169,950,841],{"class":205},[169,952,953],{"class":348}," enumerate",[169,955,956],{"class":209},"(products, ",[169,958,959],{"class":315},"start",[169,961,245],{"class":205},[169,963,964],{"class":348},"2",[169,966,967],{"class":209},"):\n",[169,969,970,973,975,978,980,983,985,987,989,991],{"class":90,"line":366},[169,971,972],{"class":209},"    lookups[",[169,974,813],{"class":205},[169,976,977],{"class":178},"\"A",[169,979,820],{"class":819},[169,981,982],{"class":209},"i",[169,984,829],{"class":819},[169,986,832],{"class":178},[169,988,935],{"class":209},[169,990,245],{"class":205},[169,992,993],{"class":209}," sku\n",[169,995,996,999,1001,1004],{"class":90,"line":372},[169,997,998],{"class":209},"lookups.sheet_state ",[169,1000,245],{"class":205},[169,1002,1003],{"class":178}," \"hidden\"",[169,1005,1006],{"class":569},"        # out of the way, still readable by the rule\n",[169,1008,1009],{"class":90,"line":378},[169,1010,236],{"emptyLinePlaceholder":235},[169,1012,1013,1015,1017],{"class":90,"line":389},[169,1014,497],{"class":209},[169,1016,245],{"class":205},[169,1018,309],{"class":209},[169,1020,1021,1023,1025,1027],{"class":90,"line":394},[169,1022,316],{"class":315},[169,1024,245],{"class":205},[169,1026,321],{"class":178},[169,1028,324],{"class":209},[169,1030,1031,1033,1035,1037,1040,1042,1045,1048,1051,1054,1056,1058],{"class":90,"line":607},[169,1032,330],{"class":315},[169,1034,245],{"class":205},[169,1036,813],{"class":205},[169,1038,1039],{"class":178},"\"'Lookups'!$A$2:$A$",[169,1041,820],{"class":819},[169,1043,1044],{"class":348},"len",[169,1046,1047],{"class":209},"(products) ",[169,1049,1050],{"class":205},"+",[169,1052,1053],{"class":348}," 1",[169,1055,829],{"class":819},[169,1057,832],{"class":178},[169,1059,324],{"class":209},[169,1061,1062,1064,1066,1068],{"class":90,"line":618},[169,1063,343],{"class":315},[169,1065,245],{"class":205},[169,1067,349],{"class":348},[169,1069,324],{"class":209},[169,1071,1072,1074,1076,1078],{"class":90,"line":623},[169,1073,357],{"class":315},[169,1075,245],{"class":205},[169,1077,349],{"class":348},[169,1079,324],{"class":209},[169,1081,1082],{"class":90,"line":629},[169,1083,369],{"class":209},[169,1085,1086],{"class":90,"line":639},[169,1087,1088],{"class":209},"orders.add_data_validation(dv)\n",[169,1090,1092,1094,1097],{"class":90,"line":1091},24,[169,1093,632],{"class":209},[169,1095,1096],{"class":178},"\"B2:B1000\"",[169,1098,369],{"class":209},[169,1100,1102],{"class":90,"line":1101},25,[169,1103,236],{"emptyLinePlaceholder":235},[169,1105,1107,1109,1112],{"class":90,"line":1106},26,[169,1108,397],{"class":209},[169,1110,1111],{"class":178},"\"dropdown_lookup.xlsx\"",[169,1113,369],{"class":209},[10,1115,1116,1117,1120],{},"Hiding the lookup sheet keeps the workbook tidy; the rule still resolves because hidden sheets are perfectly readable to formulas. Do not use ",[14,1118,1119],{},"sheet_state = \"veryHidden\""," if a user may need to add a value — it cannot be unhidden from Excel's interface.",[155,1122,1124],{"id":1123},"point-the-rule-at-a-named-range","Point the rule at a named range",[10,1126,1127,1128,1132],{},"Better still, give the range a ",[27,1129,1131],{"href":1130},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fcreate-named-range-in-excel-with-openpyxl\u002F","defined name"," and reference that. The rule then survives the list moving, and one regeneration updates every dropdown that uses it:",[160,1134,1136],{"className":196,"code":1135,"language":198,"meta":165,"style":165},"from openpyxl import Workbook\nfrom openpyxl.workbook.defined_name import DefinedName\nfrom openpyxl.worksheet.datavalidation import DataValidation\n\nregions = [\"North\", \"South\", \"West\", \"Central\"]\n\nwb = Workbook()\norders = wb.active\norders.title = \"Orders\"\norders.append([\"Order_ID\", \"Region\"])\n\nlookups = wb.create_sheet(\"Lookups\")\nfor i, region in enumerate(regions, start=2):\n    lookups[f\"A{i}\"] = region\nlookups[\"A1\"] = \"Region\"\n\nwb.defined_names.add(\n    DefinedName(\"Regions\", attr_text=f\"'Lookups'!$A$2:$A${len(regions) + 1}\")\n)\n\ndv = DataValidation(type=\"list\", formula1=\"=Regions\", allow_blank=True, showErrorMessage=True)\norders.add_data_validation(dv)\ndv.add(\"B2:B500\")\n\nwb.save(\"dropdown_named.xlsx\")\n",[14,1137,1138,1148,1160,1170,1174,1203,1207,1215,1223,1231,1243,1247,1259,1281,1304,1317,1321,1326,1362,1366,1370,1415,1419,1427,1431],{"__ignoreMap":165},[169,1139,1140,1142,1144,1146],{"class":90,"line":171},[169,1141,206],{"class":205},[169,1143,210],{"class":209},[169,1145,213],{"class":205},[169,1147,216],{"class":209},[169,1149,1150,1152,1155,1157],{"class":90,"line":219},[169,1151,206],{"class":205},[169,1153,1154],{"class":209}," openpyxl.workbook.defined_name ",[169,1156,213],{"class":205},[169,1158,1159],{"class":209}," DefinedName\n",[169,1161,1162,1164,1166,1168],{"class":90,"line":232},[169,1163,206],{"class":205},[169,1165,224],{"class":209},[169,1167,213],{"class":205},[169,1169,229],{"class":209},[169,1171,1172],{"class":90,"line":239},[169,1173,236],{"emptyLinePlaceholder":235},[169,1175,1176,1178,1180,1182,1185,1187,1190,1192,1195,1197,1200],{"class":90,"line":251},[169,1177,304],{"class":209},[169,1179,245],{"class":205},[169,1181,810],{"class":209},[169,1183,1184],{"class":178},"\"North\"",[169,1186,282],{"class":209},[169,1188,1189],{"class":178},"\"South\"",[169,1191,282],{"class":209},[169,1193,1194],{"class":178},"\"West\"",[169,1196,282],{"class":209},[169,1198,1199],{"class":178},"\"Central\"",[169,1201,1202],{"class":209},"]\n",[169,1204,1205],{"class":90,"line":262},[169,1206,236],{"emptyLinePlaceholder":235},[169,1208,1209,1211,1213],{"class":90,"line":273},[169,1210,242],{"class":209},[169,1212,245],{"class":205},[169,1214,248],{"class":209},[169,1216,1217,1219,1221],{"class":90,"line":296},[169,1218,878],{"class":209},[169,1220,245],{"class":205},[169,1222,259],{"class":209},[169,1224,1225,1227,1229],{"class":90,"line":301},[169,1226,887],{"class":209},[169,1228,245],{"class":205},[169,1230,270],{"class":178},[169,1232,1233,1235,1237,1239,1241],{"class":90,"line":312},[169,1234,896],{"class":209},[169,1236,279],{"class":178},[169,1238,282],{"class":209},[169,1240,285],{"class":178},[169,1242,293],{"class":209},[169,1244,1245],{"class":90,"line":327},[169,1246,236],{"emptyLinePlaceholder":235},[169,1248,1249,1251,1253,1255,1257],{"class":90,"line":340},[169,1250,914],{"class":209},[169,1252,245],{"class":205},[169,1254,919],{"class":209},[169,1256,922],{"class":178},[169,1258,369],{"class":209},[169,1260,1261,1263,1266,1268,1270,1273,1275,1277,1279],{"class":90,"line":354},[169,1262,945],{"class":205},[169,1264,1265],{"class":209}," i, region ",[169,1267,841],{"class":205},[169,1269,953],{"class":348},[169,1271,1272],{"class":209},"(regions, ",[169,1274,959],{"class":315},[169,1276,245],{"class":205},[169,1278,964],{"class":348},[169,1280,967],{"class":209},[169,1282,1283,1285,1287,1289,1291,1293,1295,1297,1299,1301],{"class":90,"line":366},[169,1284,972],{"class":209},[169,1286,813],{"class":205},[169,1288,977],{"class":178},[169,1290,820],{"class":819},[169,1292,982],{"class":209},[169,1294,829],{"class":819},[169,1296,832],{"class":178},[169,1298,935],{"class":209},[169,1300,245],{"class":205},[169,1302,1303],{"class":209}," region\n",[169,1305,1306,1308,1310,1312,1314],{"class":90,"line":372},[169,1307,929],{"class":209},[169,1309,932],{"class":178},[169,1311,935],{"class":209},[169,1313,245],{"class":205},[169,1315,1316],{"class":178}," \"Region\"\n",[169,1318,1319],{"class":90,"line":378},[169,1320,236],{"emptyLinePlaceholder":235},[169,1322,1323],{"class":90,"line":389},[169,1324,1325],{"class":209},"wb.defined_names.add(\n",[169,1327,1328,1331,1334,1336,1339,1341,1343,1345,1347,1349,1352,1354,1356,1358,1360],{"class":90,"line":394},[169,1329,1330],{"class":209},"    DefinedName(",[169,1332,1333],{"class":178},"\"Regions\"",[169,1335,282],{"class":209},[169,1337,1338],{"class":315},"attr_text",[169,1340,245],{"class":205},[169,1342,813],{"class":205},[169,1344,1039],{"class":178},[169,1346,820],{"class":819},[169,1348,1044],{"class":348},[169,1350,1351],{"class":209},"(regions) ",[169,1353,1050],{"class":205},[169,1355,1053],{"class":348},[169,1357,829],{"class":819},[169,1359,832],{"class":178},[169,1361,369],{"class":209},[169,1363,1364],{"class":90,"line":607},[169,1365,369],{"class":209},[169,1367,1368],{"class":90,"line":618},[169,1369,236],{"emptyLinePlaceholder":235},[169,1371,1372,1374,1376,1379,1382,1384,1386,1388,1390,1392,1395,1397,1400,1402,1404,1406,1409,1411,1413],{"class":90,"line":623},[169,1373,497],{"class":209},[169,1375,245],{"class":205},[169,1377,1378],{"class":209}," DataValidation(",[169,1380,1381],{"class":315},"type",[169,1383,245],{"class":205},[169,1385,321],{"class":178},[169,1387,282],{"class":209},[169,1389,192],{"class":315},[169,1391,245],{"class":205},[169,1393,1394],{"class":178},"\"=Regions\"",[169,1396,282],{"class":209},[169,1398,1399],{"class":315},"allow_blank",[169,1401,245],{"class":205},[169,1403,349],{"class":348},[169,1405,282],{"class":209},[169,1407,1408],{"class":315},"showErrorMessage",[169,1410,245],{"class":205},[169,1412,349],{"class":348},[169,1414,369],{"class":209},[169,1416,1417],{"class":90,"line":629},[169,1418,1088],{"class":209},[169,1420,1421,1423,1425],{"class":90,"line":639},[169,1422,632],{"class":209},[169,1424,384],{"class":178},[169,1426,369],{"class":209},[169,1428,1429],{"class":90,"line":1091},[169,1430,236],{"emptyLinePlaceholder":235},[169,1432,1433,1435,1438],{"class":90,"line":1101},[169,1434,397],{"class":209},[169,1436,1437],{"class":178},"\"dropdown_named.xlsx\"",[169,1439,369],{"class":209},[155,1441,1443],{"id":1442},"one-rule-several-ranges-and-other-rule-types","One rule, several ranges — and other rule types",[10,1445,1446,1447,1449,1450,1453],{},"A single ",[14,1448,24],{}," can cover any number of ranges, and ",[14,1451,1452],{},"list"," is only one of several types. A form usually needs a mix:",[160,1455,1457],{"className":196,"code":1456,"language":198,"meta":165,"style":165},"import datetime as dt\nfrom openpyxl import Workbook\nfrom openpyxl.worksheet.datavalidation import DataValidation\n\nwb = Workbook()\nws = wb.active\nws.append([\"Order_ID\", \"Region\", \"Quantity\", \"Order_Date\", \"Notes\"])\n\nregion = DataValidation(type=\"list\", formula1='\"North,South,West,Central\"', showErrorMessage=True)\nquantity = DataValidation(type=\"whole\", operator=\"greaterThan\", formula1=0, showErrorMessage=True)\norder_date = DataValidation(\n    type=\"date\",\n    operator=\"between\",\n    formula1=dt.date(2026, 1, 1),\n    formula2=dt.date(2026, 12, 31),\n    showErrorMessage=True,\n)\nnotes = DataValidation(type=\"textLength\", operator=\"lessThanOrEqual\", formula1=200)\n\nfor dv in (region, quantity, order_date, notes):\n    ws.add_data_validation(dv)\n\nregion.add(\"B2:B500\")\nquantity.add(\"C2:C500\")\norder_date.add(\"D2:D500\")\nnotes.add(\"E2:E500\")\nnotes.add(\"G2:G500\")        # the same rule, a second range\n\nwb.save(\"form_rules.xlsx\")\nprint(len(ws.data_validations.dataValidation), \"rule(s) written\")\n",[14,1458,1459,1472,1482,1492,1496,1504,1512,1538,1542,1575,1619,1628,1639,1651,1674,1697,1707,1711,1746,1750,1762,1767,1771,1780,1790,1800,1810,1824,1829,1839],{"__ignoreMap":165},[169,1460,1461,1463,1466,1469],{"class":90,"line":171},[169,1462,213],{"class":205},[169,1464,1465],{"class":209}," datetime ",[169,1467,1468],{"class":205},"as",[169,1470,1471],{"class":209}," dt\n",[169,1473,1474,1476,1478,1480],{"class":90,"line":219},[169,1475,206],{"class":205},[169,1477,210],{"class":209},[169,1479,213],{"class":205},[169,1481,216],{"class":209},[169,1483,1484,1486,1488,1490],{"class":90,"line":232},[169,1485,206],{"class":205},[169,1487,224],{"class":209},[169,1489,213],{"class":205},[169,1491,229],{"class":209},[169,1493,1494],{"class":90,"line":239},[169,1495,236],{"emptyLinePlaceholder":235},[169,1497,1498,1500,1502],{"class":90,"line":251},[169,1499,242],{"class":209},[169,1501,245],{"class":205},[169,1503,248],{"class":209},[169,1505,1506,1508,1510],{"class":90,"line":262},[169,1507,254],{"class":209},[169,1509,245],{"class":205},[169,1511,259],{"class":209},[169,1513,1514,1516,1518,1520,1522,1524,1526,1528,1531,1533,1536],{"class":90,"line":273},[169,1515,276],{"class":209},[169,1517,279],{"class":178},[169,1519,282],{"class":209},[169,1521,285],{"class":178},[169,1523,282],{"class":209},[169,1525,290],{"class":178},[169,1527,282],{"class":209},[169,1529,1530],{"class":178},"\"Order_Date\"",[169,1532,282],{"class":209},[169,1534,1535],{"class":178},"\"Notes\"",[169,1537,293],{"class":209},[169,1539,1540],{"class":90,"line":296},[169,1541,236],{"emptyLinePlaceholder":235},[169,1543,1544,1547,1549,1551,1553,1555,1557,1559,1561,1563,1565,1567,1569,1571,1573],{"class":90,"line":301},[169,1545,1546],{"class":209},"region ",[169,1548,245],{"class":205},[169,1550,1378],{"class":209},[169,1552,1381],{"class":315},[169,1554,245],{"class":205},[169,1556,321],{"class":178},[169,1558,282],{"class":209},[169,1560,192],{"class":315},[169,1562,245],{"class":205},[169,1564,335],{"class":178},[169,1566,282],{"class":209},[169,1568,1408],{"class":315},[169,1570,245],{"class":205},[169,1572,349],{"class":348},[169,1574,369],{"class":209},[169,1576,1577,1580,1582,1584,1586,1588,1591,1593,1596,1598,1601,1603,1605,1607,1609,1611,1613,1615,1617],{"class":90,"line":312},[169,1578,1579],{"class":209},"quantity ",[169,1581,245],{"class":205},[169,1583,1378],{"class":209},[169,1585,1381],{"class":315},[169,1587,245],{"class":205},[169,1589,1590],{"class":178},"\"whole\"",[169,1592,282],{"class":209},[169,1594,1595],{"class":315},"operator",[169,1597,245],{"class":205},[169,1599,1600],{"class":178},"\"greaterThan\"",[169,1602,282],{"class":209},[169,1604,192],{"class":315},[169,1606,245],{"class":205},[169,1608,54],{"class":348},[169,1610,282],{"class":209},[169,1612,1408],{"class":315},[169,1614,245],{"class":205},[169,1616,349],{"class":348},[169,1618,369],{"class":209},[169,1620,1621,1624,1626],{"class":90,"line":327},[169,1622,1623],{"class":209},"order_date ",[169,1625,245],{"class":205},[169,1627,309],{"class":209},[169,1629,1630,1632,1634,1637],{"class":90,"line":340},[169,1631,316],{"class":315},[169,1633,245],{"class":205},[169,1635,1636],{"class":178},"\"date\"",[169,1638,324],{"class":209},[169,1640,1641,1644,1646,1649],{"class":90,"line":354},[169,1642,1643],{"class":315},"    operator",[169,1645,245],{"class":205},[169,1647,1648],{"class":178},"\"between\"",[169,1650,324],{"class":209},[169,1652,1653,1655,1657,1660,1663,1665,1667,1669,1671],{"class":90,"line":366},[169,1654,330],{"class":315},[169,1656,245],{"class":205},[169,1658,1659],{"class":209},"dt.date(",[169,1661,1662],{"class":348},"2026",[169,1664,282],{"class":209},[169,1666,850],{"class":348},[169,1668,282],{"class":209},[169,1670,850],{"class":348},[169,1672,1673],{"class":209},"),\n",[169,1675,1676,1679,1681,1683,1685,1687,1690,1692,1695],{"class":90,"line":372},[169,1677,1678],{"class":315},"    formula2",[169,1680,245],{"class":205},[169,1682,1659],{"class":209},[169,1684,1662],{"class":348},[169,1686,282],{"class":209},[169,1688,1689],{"class":348},"12",[169,1691,282],{"class":209},[169,1693,1694],{"class":348},"31",[169,1696,1673],{"class":209},[169,1698,1699,1701,1703,1705],{"class":90,"line":378},[169,1700,357],{"class":315},[169,1702,245],{"class":205},[169,1704,349],{"class":348},[169,1706,324],{"class":209},[169,1708,1709],{"class":90,"line":389},[169,1710,369],{"class":209},[169,1712,1713,1716,1718,1720,1722,1724,1727,1729,1731,1733,1736,1738,1740,1742,1744],{"class":90,"line":394},[169,1714,1715],{"class":209},"notes ",[169,1717,245],{"class":205},[169,1719,1378],{"class":209},[169,1721,1381],{"class":315},[169,1723,245],{"class":205},[169,1725,1726],{"class":178},"\"textLength\"",[169,1728,282],{"class":209},[169,1730,1595],{"class":315},[169,1732,245],{"class":205},[169,1734,1735],{"class":178},"\"lessThanOrEqual\"",[169,1737,282],{"class":209},[169,1739,192],{"class":315},[169,1741,245],{"class":205},[169,1743,104],{"class":348},[169,1745,369],{"class":209},[169,1747,1748],{"class":90,"line":607},[169,1749,236],{"emptyLinePlaceholder":235},[169,1751,1752,1754,1757,1759],{"class":90,"line":618},[169,1753,945],{"class":205},[169,1755,1756],{"class":209}," dv ",[169,1758,841],{"class":205},[169,1760,1761],{"class":209}," (region, quantity, order_date, notes):\n",[169,1763,1764],{"class":90,"line":623},[169,1765,1766],{"class":209},"    ws.add_data_validation(dv)\n",[169,1768,1769],{"class":90,"line":629},[169,1770,236],{"emptyLinePlaceholder":235},[169,1772,1773,1776,1778],{"class":90,"line":639},[169,1774,1775],{"class":209},"region.add(",[169,1777,384],{"class":178},[169,1779,369],{"class":209},[169,1781,1782,1785,1788],{"class":90,"line":1091},[169,1783,1784],{"class":209},"quantity.add(",[169,1786,1787],{"class":178},"\"C2:C500\"",[169,1789,369],{"class":209},[169,1791,1792,1795,1798],{"class":90,"line":1101},[169,1793,1794],{"class":209},"order_date.add(",[169,1796,1797],{"class":178},"\"D2:D500\"",[169,1799,369],{"class":209},[169,1801,1802,1805,1808],{"class":90,"line":1106},[169,1803,1804],{"class":209},"notes.add(",[169,1806,1807],{"class":178},"\"E2:E500\"",[169,1809,369],{"class":209},[169,1811,1813,1815,1818,1821],{"class":90,"line":1812},27,[169,1814,1804],{"class":209},[169,1816,1817],{"class":178},"\"G2:G500\"",[169,1819,1820],{"class":209},")        ",[169,1822,1823],{"class":569},"# the same rule, a second range\n",[169,1825,1827],{"class":90,"line":1826},28,[169,1828,236],{"emptyLinePlaceholder":235},[169,1830,1832,1834,1837],{"class":90,"line":1831},29,[169,1833,397],{"class":209},[169,1835,1836],{"class":178},"\"form_rules.xlsx\"",[169,1838,369],{"class":209},[169,1840,1842,1845,1847,1849,1852,1855],{"class":90,"line":1841},30,[169,1843,1844],{"class":348},"print",[169,1846,847],{"class":209},[169,1848,1044],{"class":348},[169,1850,1851],{"class":209},"(ws.data_validations.dataValidation), ",[169,1853,1854],{"class":178},"\"rule(s) written\"",[169,1856,369],{"class":209},[10,1858,1859,282,1862,282,1865,282,1868,282,1871,1874,1875,1878,1879,1881,1882,1885,1886,1889],{},[14,1860,1861],{},"whole",[14,1863,1864],{},"decimal",[14,1866,1867],{},"date",[14,1869,1870],{},"time",[14,1872,1873],{},"textLength"," and ",[14,1876,1877],{},"custom"," complete the set. ",[14,1880,1877],{}," takes a formula that must evaluate to ",[14,1883,1884],{},"TRUE",", which is how cross-column rules are expressed — for example ",[14,1887,1888],{},"formula1=\"=D2>=C2\""," to require an end date on or after a start date.",[155,1891,1893],{"id":1892},"read-back-the-rules-in-an-existing-workbook","Read back the rules in an existing workbook",[10,1895,1896],{},"Auditing a template someone else built is a matter of walking the sheet's validation collection:",[160,1898,1900],{"className":196,"code":1899,"language":198,"meta":165,"style":165},"from openpyxl import load_workbook\n\nwb = load_workbook(\"form_rules.xlsx\")\nfor ws in wb.worksheets:\n    for dv in ws.data_validations.dataValidation:\n        print(f\"{ws.title}: {dv.type} on {dv.sqref} -> {dv.formula1}\")\n",[14,1901,1902,1913,1917,1930,1942,1954],{"__ignoreMap":165},[169,1903,1904,1906,1908,1910],{"class":90,"line":171},[169,1905,206],{"class":205},[169,1907,210],{"class":209},[169,1909,213],{"class":205},[169,1911,1912],{"class":209}," load_workbook\n",[169,1914,1915],{"class":90,"line":219},[169,1916,236],{"emptyLinePlaceholder":235},[169,1918,1919,1921,1923,1926,1928],{"class":90,"line":232},[169,1920,242],{"class":209},[169,1922,245],{"class":205},[169,1924,1925],{"class":209}," load_workbook(",[169,1927,1836],{"class":178},[169,1929,369],{"class":209},[169,1931,1932,1934,1937,1939],{"class":90,"line":239},[169,1933,945],{"class":205},[169,1935,1936],{"class":209}," ws ",[169,1938,841],{"class":205},[169,1940,1941],{"class":209}," wb.worksheets:\n",[169,1943,1944,1947,1949,1951],{"class":90,"line":251},[169,1945,1946],{"class":205},"    for",[169,1948,1756],{"class":209},[169,1950,841],{"class":205},[169,1952,1953],{"class":209}," ws.data_validations.dataValidation:\n",[169,1955,1956,1959,1961,1963,1965,1967,1970,1972,1975,1977,1980,1982,1985,1987,1990,1992,1995,1997,2000,2002,2004],{"class":90,"line":262},[169,1957,1958],{"class":348},"        print",[169,1960,847],{"class":209},[169,1962,813],{"class":205},[169,1964,832],{"class":178},[169,1966,820],{"class":819},[169,1968,1969],{"class":209},"ws.title",[169,1971,829],{"class":819},[169,1973,1974],{"class":178},": ",[169,1976,820],{"class":819},[169,1978,1979],{"class":209},"dv.type",[169,1981,829],{"class":819},[169,1983,1984],{"class":178}," on ",[169,1986,820],{"class":819},[169,1988,1989],{"class":209},"dv.sqref",[169,1991,829],{"class":819},[169,1993,1994],{"class":178}," -> ",[169,1996,820],{"class":819},[169,1998,1999],{"class":209},"dv.formula1",[169,2001,829],{"class":819},[169,2003,832],{"class":178},[169,2005,369],{"class":209},[10,2007,2008],{},"That inventory answers the question that starts most template investigations: whether the file constrains anything at all, and whether the constraint matches the rules your Python job enforces. Where they disagree, the workbook and the pipeline will reject different data, and users will be told their entry is fine right up until the report fails.",[155,2010,2012],{"id":2011},"where-the-list-values-should-live","Where the list values should live",[10,2014,2015],{},"Three sources are possible, and the right one depends on how the list changes over time:",[33,2017,42,2022,42,2025,42,2028,42,2031,42,2036,42,2041,42,2048,42,2053,42,2057,42,2061,42,2065,42,2068,42,2071,42,2074,42,2077,42,2080,42,2083,42,2086,42,2089,42,2092,42,2095,42,2099,42,2102,42,2105],{"viewBox":2018,"role":36,"ariaLabelledBy":2019,"xmlns":40,"style":41},"0 0 740 226",[2020,2021],"dv-src-t","dv-src-d",[44,2023,2024],{"id":2020},"Three sources for a dropdown's values",[48,2026,2027],{"id":2021},"An inline quoted string suits a short fixed list but is capped at 255 characters. A range on a lookup sheet suits long lists but breaks when rows are added beyond the range. A named range suits lists that grow, because the name is regenerated in one place.",[52,2029],{"x":54,"y":54,"width":55,"height":2030,"fill":57},"226",[52,2032],{"x":2033,"y":2034,"width":2035,"height":709,"rx":64,"fill":65,"stroke":106},"16","26","228",[69,2037,2040],{"x":694,"y":2038,"style":2039},"54","font-size:13px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","inline string",[52,2042],{"x":2043,"y":2044,"width":2045,"height":2046,"rx":2047,"fill":57,"stroke":106},"36","68","188","34","7",[69,2049,2052],{"x":694,"y":2050,"style":2051},"90","font-size:11px;fill:var(--text,#172033);text-anchor:middle;font-family:ui-monospace,Menlo,monospace","'\"North,South,West\"'",[69,2054,2056],{"x":694,"y":116,"style":2055},"font-size:11.5px;fill:var(--teal,#0f9488);text-anchor:middle","simplest to write",[69,2058,2060],{"x":694,"y":2059,"style":117},"150","255-character cap",[69,2062,2064],{"x":694,"y":2063,"style":78},"180","use for short, stable lists",[52,2066],{"x":2067,"y":2034,"width":2035,"height":709,"rx":64,"fill":105,"stroke":106},"256",[69,2069,2070],{"x":109,"y":2038,"style":2039},"range on a sheet",[52,2072],{"x":2073,"y":2044,"width":2045,"height":2046,"rx":2047,"fill":57,"stroke":106},"276",[69,2075,2076],{"x":109,"y":2050,"style":2051},"'Lookups'!$A$2:$A$121",[69,2078,2079],{"x":109,"y":116,"style":2055},"no length limit",[69,2081,2082],{"x":109,"y":2059,"style":117},"breaks past the last row",[69,2084,2085],{"x":109,"y":2063,"style":78},"use for long fixed lists",[52,2087],{"x":2088,"y":2034,"width":2035,"height":709,"rx":64,"fill":133,"stroke":134,"style":67},"496",[69,2090,2091],{"x":748,"y":2038,"style":138},"named range",[52,2093],{"x":2094,"y":2044,"width":2045,"height":2046,"rx":2047,"fill":57,"stroke":106},"516",[69,2096,2098],{"x":748,"y":2050,"style":2097},"font-size:11px;fill:var(--teal,#0f9488);text-anchor:middle;font-family:ui-monospace,Menlo,monospace","=Regions",[69,2100,2101],{"x":748,"y":116,"style":2055},"one place to repoint",[69,2103,2104],{"x":748,"y":2059,"style":2055},"readable in the file",[69,2106,2107],{"x":748,"y":2063,"style":78},"use for lists that grow",[10,2109,2110],{},"The middle option has a failure mode worth naming: when the lookup list grows from 120 to 140 rows and the range still ends at row 121, the twenty new values are missing from every dropdown and nothing reports it. Regenerating the reference — or the name — in the same script that writes the list is what keeps the two in step, and it is the reason the named-range version is worth the extra three lines in anything that runs monthly.",[155,2112,2114],{"id":2113},"common-pitfalls-and-gotchas","Common pitfalls and gotchas",[2116,2117,2118,2134],"table",{},[2119,2120,2121],"thead",{},[2122,2123,2124,2128,2131],"tr",{},[2125,2126,2127],"th",{},"Symptom",[2125,2129,2130],{},"Cause",[2125,2132,2133],{},"Fix",[2135,2136,2137,2154,2165,2179,2194,2205],"tbody",{},[2122,2138,2139,2143,2146],{},[2140,2141,2142],"td",{},"No dropdown appears",[2140,2144,2145],{},"Rule never registered on the sheet",[2140,2147,2148,2149,2151,2152],{},"Call ",[14,2150,113],{}," before ",[14,2153,423],{},[2122,2155,2156,2159,2162],{},[2140,2157,2158],{},"Long list silently ignored",[2140,2160,2161],{},"Inline list over 255 characters",[2140,2163,2164],{},"Move values to a lookup sheet or named range",[2122,2166,2167,2170,2173],{},[2140,2168,2169],{},"Excel repairs the file",[2140,2171,2172],{},"Missing quotes around an inline list",[2140,2174,2175,2178],{},[14,2176,2177],{},"formula1='\"North,South\"'",", quotes included",[2122,2180,2181,2184,2191],{},[2140,2182,2183],{},"Rule lost after rewriting the sheet",[2140,2185,2186,2187,2190],{},"pandas ",[14,2188,2189],{},"to_excel"," replaced the sheet",[2140,2192,2193],{},"Apply validation after the data is written",[2122,2195,2196,2199,2202],{},[2140,2197,2198],{},"Pasted values bypass the rule",[2140,2200,2201],{},"Excel validates typing, not pasting",[2140,2203,2204],{},"Keep the Python-side checks",[2122,2206,2207,2210,2213],{},[2140,2208,2209],{},"Dropdown empty on another sheet",[2140,2211,2212],{},"Range reference not sheet-qualified",[2140,2214,2215,2216,2219],{},"Use ",[14,2217,2218],{},"'Lookups'!$A$2:$A$40"," or a defined name",[155,2221,2223],{"id":2222},"performance-and-scale-notes","Performance and scale notes",[10,2225,2226,2227,2230,2231,2234],{},"Validation rules cost almost nothing to store or evaluate: each is a few lines of XML regardless of how many cells it covers, and Excel checks the rule only when a cell is edited rather than on every recalculation, so ",[14,2228,2229],{},"dv.add(\"B2:B100000\")"," is no more expensive than ",[14,2232,2233],{},"dv.add(\"B2:B10\")",". Prefer one rule over a whole column to thousands of per-cell rules — a workbook carrying ten thousand separate validation entries opens slowly and is painful to maintain in Excel's interface. When several columns share a constraint, add multiple ranges to a single rule rather than duplicating the object.",[155,2236,2238],{"id":2237},"conclusion","Conclusion",[10,2240,2241],{},"A dropdown is three calls in a fixed order — construct the rule, register it on the sheet, attach the ranges — and the rest is choosing where the values live. Inline lists are fine for a handful of stable options; anything longer belongs on a lookup sheet, ideally behind a named range so the rule survives the list growing. Write a real error message, pick an error style that matches how firm the rule really is, and remember that Excel enforces this at the keyboard only: your Python validation still has to run.",[155,2243,2245],{"id":2244},"frequently-asked-questions","Frequently asked questions",[10,2247,2248,2252,2253,2255],{},[2249,2250,2251],"strong",{},"Why does my dropdown disappear when the list gets long?","\nAn inline list is stored as a single formula string limited to 255 characters. Put the values on a lookup sheet and point ",[14,2254,192],{}," at that range instead.",[10,2257,2258,2261,2262,2264,2265,2267],{},[2249,2259,2260],{},"Do I need the quotes around an inline list?","\nYes. ",[14,2263,192],{}," must be a quoted, comma-separated string such as ",[14,2266,2052],{},". Without the quotes Excel reads it as a reference and shows an error.",[10,2269,2270,2273,2274,2276,2277,2279],{},[2249,2271,2272],{},"Can the source list live on another sheet?","\nYes in modern Excel — point at ",[14,2275,2218],{},", or better, define a name for the range and use ",[14,2278,2098],{}," so a growing list does not break the rule.",[10,2281,2282,2285,2286,2288],{},[2249,2283,2284],{},"Does openpyxl stop me writing a value that breaks the rule?","\nNo. ",[14,2287,24],{}," is metadata for Excel's interface only, so keep validating values in Python as well.",[155,2290,2292],{"id":2291},"related","Related",[10,2294,2295],{},"Up to the parent guide:",[2297,2298,2299],"ul",{},[2300,2301,2302,2304],"li",{},[27,2303,30],{"href":29}," — the full validation layer, in Python and in the workbook.",[10,2306,2307],{},"Related guides:",[2297,2309,2310,2316,2323,2330],{},[2300,2311,2312,2315],{},[27,2313,2314],{"href":1130},"Create a Named Range in Excel with openpyxl"," — the cleanest source for a dropdown list.",[2300,2317,2318,2322],{},[27,2319,2321],{"href":2320},"\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"," — marking values that already broke the rules.",[2300,2324,2325,2329],{},[27,2326,2328],{"href":2327},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002F","Applying Conditional Formatting with openpyxl"," — colour rules that react to the same conditions.",[2300,2331,2332,2336],{},[27,2333,2335],{"href":2334},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Ffill-excel-template-with-python-openpyxl\u002F","Fill an Excel Template with Python and openpyxl"," — keeping template rules intact while writing data.",[2338,2339,2340],"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 .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}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}",{"title":165,"searchDepth":219,"depth":219,"links":2342},[2343,2344,2345,2346,2347,2348,2349,2350,2351,2352,2353,2354,2355],{"id":157,"depth":219,"text":158},{"id":185,"depth":219,"text":186},{"id":427,"depth":219,"text":428},{"id":767,"depth":219,"text":768},{"id":1123,"depth":219,"text":1124},{"id":1442,"depth":219,"text":1443},{"id":1892,"depth":219,"text":1893},{"id":2011,"depth":219,"text":2012},{"id":2113,"depth":219,"text":2114},{"id":2222,"depth":219,"text":2223},{"id":2237,"depth":219,"text":2238},{"id":2244,"depth":219,"text":2245},{"id":2291,"depth":219,"text":2292},"2026-08-01","Write Excel dropdown lists from Python with openpyxl DataValidation — inline lists, lists from a lookup sheet or named range, custom error messages, and the 255-character limit.","md",[2360,2362,2364,2366],{"q":2251,"a":2361},"An inline list is stored as a single formula string limited to 255 characters. Put the values on a lookup sheet and point formula1 at that range instead.",{"q":2260,"a":2363},"Yes. formula1 must be a quoted, comma-separated string such as '\"North,South,West\"'. Without the quotes Excel reads it as a reference and shows an error.",{"q":2272,"a":2365},"Yes in modern Excel — point at 'Lookups'!$A$2:$A$40, or better, define a name for the range and use =Regions so a growing list does not break the rule.",{"q":2284,"a":2367},"No. DataValidation is metadata for Excel's interface only, so keep validating values in Python as well.",{"breadcrumb":2369},[2370,2373,2376,2377],{"name":2371,"item":2372},"Home","\u002F",{"name":2374,"item":2375},"Advanced Data Transformation and Cleaning","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":30,"item":29},{"name":5,"item":2378},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fadd-dropdown-data-validation-to-excel-with-openpyxl\u002F","\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fadd-dropdown-data-validation-to-excel-with-openpyxl",{"title":5,"description":2381},"Create list validation in Excel from Python: inline value lists, ranges and named ranges, allow_blank, input prompts, error styles, and applying a rule to many ranges.","add-dropdown-data-validation-to-excel-with-openpyxl","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fadd-dropdown-data-validation-to-excel-with-openpyxl\u002Findex","how-to","FedUPk01XAUDGT7fHJtNFb2uJYSo4S60Ln1crCZNUA8",[2387,2390],{"title":30,"path":2388,"stem":2389,"children":-1},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Findex",{"title":2391,"path":2392,"stem":2393,"children":-1},"Check Excel Data Types with pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcheck-excel-data-types-with-pandas","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcheck-excel-data-types-with-pandas\u002Findex",1785584462714]