[{"data":1,"prerenderedAt":2339},["ShallowReactive",2],{"doc:\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Flock-cells-and-protect-a-sheet-with-openpyxl":3,"surround:\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Flock-cells-and-protect-a-sheet-with-openpyxl":2332},{"id":4,"title":5,"body":6,"dateModified":2308,"datePublished":2308,"description":2309,"extension":2310,"faq":2311,"meta":2323,"navigation":263,"path":2324,"seo":2325,"slug":2328,"stem":2329,"type":2330,"__hash__":2331},"docs\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Flock-cells-and-protect-a-sheet-with-openpyxl\u002Findex.md","Lock Cells and Protect a Sheet with openpyxl",{"type":7,"value":8,"toc":2295},"minimark",[9,24,173,178,204,215,219,222,678,682,685,904,911,918,921,967,971,1044,1057,1182,1198,1305,1315,1319,1326,1487,1494,1498,1501,1579,1851,1871,1875,2024,2028,2037,2140,2143,2153,2160,2164,2188,2192,2205,2211,2231,2240,2249,2253,2291],[10,11,12,13,17,18,23],"p",{},"Ship a report with a formula column and somebody will type over it. Not maliciously — they will tab one cell too far, or paste a block that lands one column left of where they meant. Excel's sheet protection exists for exactly this, and openpyxl sets it in a few lines. The catch is that the mechanic runs backwards from how people expect: you do not lock the formulas, you ",[14,15,16],"em",{},"unlock"," the inputs. This guide covers the pattern, the inverted permission flags, partial protection, and verifying the result. It is the practical core of ",[19,20,22],"a",{"href":21},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002F","Protecting and Sharing Excel Workbooks",".",[25,26,35,36,35,40,35,44,35,51,35,58,35,68,35,74,35,79,35,84,35,88,35,92,35,96,35,100,35,107,35,112,35,114,35,117,35,119,35,122,35,124,35,127,35,130,35,134,35,136,35,138,35,140,35,143,35,145,35,148,35,153,35,158,35,163,35,166,35,170],"svg",{"viewBox":27,"role":28,"ariaLabel":29,"ariaLabelledBy":30,"xmlns":33,"style":34},"0 0 800 250","img","A protected forecast sheet: locked label and formula columns shown in grey, and a single unlocked input column tinted so readers can see where they may type.",[31,32],"prot-t","prot-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:800px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[37,38,39],"title",{"id":31},"What a well-protected report sheet looks like to a reader",[41,42,43],"desc",{"id":32},"A four-column sheet. The Region, Unit price and Revenue columns are locked and shown plain; the Units column is unlocked and tinted amber so it is obvious where input is expected. The Revenue column holds formulas that reference the input column, so the numbers update as the reader types, while the formulas themselves cannot be overwritten.",[45,46],"rect",{"x":47,"y":47,"width":48,"height":49,"fill":50},"0","800","250","#ffffff",[52,53,57],"text",{"x":54,"y":55,"style":56},"400","26","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","only the tinted column accepts typing",[45,59],{"x":60,"y":61,"width":62,"height":63,"rx":64,"fill":65,"stroke":66,"style":67},"40","38","180","34","6","#ebebfd","var(--brand,#5b5cf0)","stroke-width:2px",[52,69,73],{"x":70,"y":71,"style":72},"130","61","font-size:11.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Region",[45,75],{"x":76,"y":61,"width":62,"height":63,"rx":64,"fill":77,"stroke":78,"style":67},"228","#fdefd8","var(--gold,#b4740a)",[52,80,83],{"x":81,"y":71,"style":82},"318","font-size:11.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Units (enter)",[45,85],{"x":86,"y":61,"width":87,"height":63,"rx":64,"fill":65,"stroke":66,"style":67},"416","164",[52,89,91],{"x":90,"y":71,"style":72},"498","Unit price",[45,93],{"x":94,"y":61,"width":95,"height":63,"rx":64,"fill":65,"stroke":66,"style":67},"588","172",[52,97,99],{"x":98,"y":71,"style":72},"674","Revenue",[45,101],{"x":60,"y":102,"width":62,"height":103,"rx":104,"fill":105,"stroke":106},"78","30","5","#f0f2f5","var(--line,#cdd5e6)",[52,108,111],{"x":70,"y":109,"style":110},"98","font-size:11px;fill:var(--text,#172033);text-anchor:middle","North",[45,113],{"x":76,"y":102,"width":62,"height":103,"rx":104,"fill":77,"stroke":78,"style":67},[52,115,47],{"x":81,"y":109,"style":116},"font-size:11px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",[45,118],{"x":86,"y":102,"width":87,"height":103,"rx":104,"fill":105,"stroke":106},[52,120,121],{"x":90,"y":109,"style":110},"12.50",[45,123],{"x":94,"y":102,"width":95,"height":103,"rx":104,"fill":105,"stroke":106},[52,125,126],{"x":98,"y":109,"style":110},"=B2*C2",[45,128],{"x":60,"y":129,"width":62,"height":103,"rx":104,"fill":105,"stroke":106},"114",[52,131,133],{"x":70,"y":132,"style":110},"134","South",[45,135],{"x":76,"y":129,"width":62,"height":103,"rx":104,"fill":77,"stroke":78,"style":67},[52,137,47],{"x":81,"y":132,"style":116},[45,139],{"x":86,"y":129,"width":87,"height":103,"rx":104,"fill":105,"stroke":106},[52,141,142],{"x":90,"y":132,"style":110},"11.00",[45,144],{"x":94,"y":129,"width":95,"height":103,"rx":104,"fill":105,"stroke":106},[52,146,147],{"x":98,"y":132,"style":110},"=B3*C3",[45,149],{"x":60,"y":95,"width":150,"height":151,"rx":152,"fill":77,"stroke":78,"style":67},"332","52","11",[52,154,157],{"x":155,"y":156,"style":82},"206","194","locked=False · tinted",[52,159,162],{"x":155,"y":160,"style":161},"213","font-size:10.5px;fill:var(--muted,#5b6780);text-anchor:middle","the reader can type here",[45,164],{"x":165,"y":95,"width":150,"height":151,"rx":152,"fill":65,"stroke":66,"style":67},"428",[52,167,169],{"x":168,"y":156,"style":72},"594","locked=True · the default",[52,171,172],{"x":168,"y":160,"style":161},"formulas and labels are safe",[174,175,177],"h2",{"id":176},"prerequisites","Prerequisites",[179,180,185],"pre",{"className":181,"code":182,"language":183,"meta":184,"style":184},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl\n","bash","",[186,187,188],"code",{"__ignoreMap":184},[189,190,193,197,201],"span",{"class":191,"line":192},"line",1,[189,194,196],{"class":195},"sMTad","pip",[189,198,200],{"class":199},"srMev"," install",[189,202,203],{"class":199}," openpyxl\n",[10,205,206,207,210,211,214],{},"One concept to hold before starting: ",[186,208,209],{},"locked"," is a ",[14,212,213],{},"cell style attribute",", and it does nothing on its own. Excel only consults it when the sheet is protected. So a workbook where every cell is locked and protection is off behaves exactly like one where nothing is locked — which is why the flag catches people out.",[174,216,218],{"id":217},"step-1-build-a-sheet-worth-protecting","Step 1 — Build a sheet worth protecting",[10,220,221],{},"A small forecast with a formula column gives the protection something to defend:",[179,223,227],{"className":224,"code":225,"language":226,"meta":184,"style":184},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import Workbook\nfrom openpyxl.styles import Font, PatternFill, Alignment\n\nwb = Workbook()\nws = wb.active\nws.title = \"Forecast\"\n\nws.append([\"Region\", \"Units (enter)\", \"Unit price\", \"Revenue\"])\nfor row, (region, price) in enumerate(\n    [(\"North\", 12.50), (\"South\", 11.00), (\"West\", 13.25)], start=2\n):\n    ws.append([region, 0, price, f\"=B{row}*C{row}\"])\n\nws.append([])\nws.append([\"Total\", None, None, \"=SUM(D2:D4)\"])\n\nheader = Font(bold=True, color=\"FFFFFF\")\nheader_fill = PatternFill(\"solid\", fgColor=\"4338CA\")\nfor cell in ws[1]:\n    cell.font = header\n    cell.fill = header_fill\n    cell.alignment = Alignment(horizontal=\"center\")\n\nfor col, width in zip(\"ABCD\", (16, 15, 13, 14)):\n    ws.column_dimensions[col].width = width\n\nwb.save(\"forecast.xlsx\")\n","python",[186,228,229,245,258,265,277,288,299,304,332,351,396,402,443,448,454,478,483,515,541,560,571,582,603,608,651,662,667],{"__ignoreMap":184},[189,230,231,235,239,242],{"class":191,"line":192},[189,232,234],{"class":233},"s-kum","from",[189,236,238],{"class":237},"skGVy"," openpyxl ",[189,240,241],{"class":233},"import",[189,243,244],{"class":237}," Workbook\n",[189,246,248,250,253,255],{"class":191,"line":247},2,[189,249,234],{"class":233},[189,251,252],{"class":237}," openpyxl.styles ",[189,254,241],{"class":233},[189,256,257],{"class":237}," Font, PatternFill, Alignment\n",[189,259,261],{"class":191,"line":260},3,[189,262,264],{"emptyLinePlaceholder":263},true,"\n",[189,266,268,271,274],{"class":191,"line":267},4,[189,269,270],{"class":237},"wb ",[189,272,273],{"class":233},"=",[189,275,276],{"class":237}," Workbook()\n",[189,278,280,283,285],{"class":191,"line":279},5,[189,281,282],{"class":237},"ws ",[189,284,273],{"class":233},[189,286,287],{"class":237}," wb.active\n",[189,289,291,294,296],{"class":191,"line":290},6,[189,292,293],{"class":237},"ws.title ",[189,295,273],{"class":233},[189,297,298],{"class":199}," \"Forecast\"\n",[189,300,302],{"class":191,"line":301},7,[189,303,264],{"emptyLinePlaceholder":263},[189,305,307,310,313,316,319,321,324,326,329],{"class":191,"line":306},8,[189,308,309],{"class":237},"ws.append([",[189,311,312],{"class":199},"\"Region\"",[189,314,315],{"class":237},", ",[189,317,318],{"class":199},"\"Units (enter)\"",[189,320,315],{"class":237},[189,322,323],{"class":199},"\"Unit price\"",[189,325,315],{"class":237},[189,327,328],{"class":199},"\"Revenue\"",[189,330,331],{"class":237},"])\n",[189,333,335,338,341,344,348],{"class":191,"line":334},9,[189,336,337],{"class":233},"for",[189,339,340],{"class":237}," row, (region, price) ",[189,342,343],{"class":233},"in",[189,345,347],{"class":346},"sP0c6"," enumerate",[189,349,350],{"class":237},"(\n",[189,352,354,357,360,362,364,367,370,372,374,376,379,381,384,387,391,393],{"class":191,"line":353},10,[189,355,356],{"class":237},"    [(",[189,358,359],{"class":199},"\"North\"",[189,361,315],{"class":237},[189,363,121],{"class":346},[189,365,366],{"class":237},"), (",[189,368,369],{"class":199},"\"South\"",[189,371,315],{"class":237},[189,373,142],{"class":346},[189,375,366],{"class":237},[189,377,378],{"class":199},"\"West\"",[189,380,315],{"class":237},[189,382,383],{"class":346},"13.25",[189,385,386],{"class":237},")], ",[189,388,390],{"class":389},"sa561","start",[189,392,273],{"class":233},[189,394,395],{"class":346},"2\n",[189,397,399],{"class":191,"line":398},11,[189,400,401],{"class":237},"):\n",[189,403,405,408,410,413,416,419,423,426,429,432,434,436,438,441],{"class":191,"line":404},12,[189,406,407],{"class":237},"    ws.append([region, ",[189,409,47],{"class":346},[189,411,412],{"class":237},", price, ",[189,414,415],{"class":233},"f",[189,417,418],{"class":199},"\"=B",[189,420,422],{"class":421},"sSjpA","{",[189,424,425],{"class":237},"row",[189,427,428],{"class":421},"}",[189,430,431],{"class":199},"*C",[189,433,422],{"class":421},[189,435,425],{"class":237},[189,437,428],{"class":421},[189,439,440],{"class":199},"\"",[189,442,331],{"class":237},[189,444,446],{"class":191,"line":445},13,[189,447,264],{"emptyLinePlaceholder":263},[189,449,451],{"class":191,"line":450},14,[189,452,453],{"class":237},"ws.append([])\n",[189,455,457,459,462,464,467,469,471,473,476],{"class":191,"line":456},15,[189,458,309],{"class":237},[189,460,461],{"class":199},"\"Total\"",[189,463,315],{"class":237},[189,465,466],{"class":346},"None",[189,468,315],{"class":237},[189,470,466],{"class":346},[189,472,315],{"class":237},[189,474,475],{"class":199},"\"=SUM(D2:D4)\"",[189,477,331],{"class":237},[189,479,481],{"class":191,"line":480},16,[189,482,264],{"emptyLinePlaceholder":263},[189,484,486,489,491,494,497,499,502,504,507,509,512],{"class":191,"line":485},17,[189,487,488],{"class":237},"header ",[189,490,273],{"class":233},[189,492,493],{"class":237}," Font(",[189,495,496],{"class":389},"bold",[189,498,273],{"class":233},[189,500,501],{"class":346},"True",[189,503,315],{"class":237},[189,505,506],{"class":389},"color",[189,508,273],{"class":233},[189,510,511],{"class":199},"\"FFFFFF\"",[189,513,514],{"class":237},")\n",[189,516,518,521,523,526,529,531,534,536,539],{"class":191,"line":517},18,[189,519,520],{"class":237},"header_fill ",[189,522,273],{"class":233},[189,524,525],{"class":237}," PatternFill(",[189,527,528],{"class":199},"\"solid\"",[189,530,315],{"class":237},[189,532,533],{"class":389},"fgColor",[189,535,273],{"class":233},[189,537,538],{"class":199},"\"4338CA\"",[189,540,514],{"class":237},[189,542,544,546,549,551,554,557],{"class":191,"line":543},19,[189,545,337],{"class":233},[189,547,548],{"class":237}," cell ",[189,550,343],{"class":233},[189,552,553],{"class":237}," ws[",[189,555,556],{"class":346},"1",[189,558,559],{"class":237},"]:\n",[189,561,563,566,568],{"class":191,"line":562},20,[189,564,565],{"class":237},"    cell.font ",[189,567,273],{"class":233},[189,569,570],{"class":237}," header\n",[189,572,574,577,579],{"class":191,"line":573},21,[189,575,576],{"class":237},"    cell.fill ",[189,578,273],{"class":233},[189,580,581],{"class":237}," header_fill\n",[189,583,585,588,590,593,596,598,601],{"class":191,"line":584},22,[189,586,587],{"class":237},"    cell.alignment ",[189,589,273],{"class":233},[189,591,592],{"class":237}," Alignment(",[189,594,595],{"class":389},"horizontal",[189,597,273],{"class":233},[189,599,600],{"class":199},"\"center\"",[189,602,514],{"class":237},[189,604,606],{"class":191,"line":605},23,[189,607,264],{"emptyLinePlaceholder":263},[189,609,611,613,616,618,621,624,627,630,633,635,638,640,643,645,648],{"class":191,"line":610},24,[189,612,337],{"class":233},[189,614,615],{"class":237}," col, width ",[189,617,343],{"class":233},[189,619,620],{"class":346}," zip",[189,622,623],{"class":237},"(",[189,625,626],{"class":199},"\"ABCD\"",[189,628,629],{"class":237},", (",[189,631,632],{"class":346},"16",[189,634,315],{"class":237},[189,636,637],{"class":346},"15",[189,639,315],{"class":237},[189,641,642],{"class":346},"13",[189,644,315],{"class":237},[189,646,647],{"class":346},"14",[189,649,650],{"class":237},")):\n",[189,652,654,657,659],{"class":191,"line":653},25,[189,655,656],{"class":237},"    ws.column_dimensions[col].width ",[189,658,273],{"class":233},[189,660,661],{"class":237}," width\n",[189,663,665],{"class":191,"line":664},26,[189,666,264],{"emptyLinePlaceholder":263},[189,668,670,673,676],{"class":191,"line":669},27,[189,671,672],{"class":237},"wb.save(",[189,674,675],{"class":199},"\"forecast.xlsx\"",[189,677,514],{"class":237},[174,679,681],{"id":680},"step-2-unlock-the-inputs-then-protect","Step 2 — Unlock the inputs, then protect",[10,683,684],{},"The two halves, in the only order that works:",[179,686,688],{"className":224,"code":687,"language":226,"meta":184,"style":184},"from openpyxl import load_workbook\nfrom openpyxl.styles import Protection, PatternFill\n\nwb = load_workbook(\"forecast.xlsx\")\nws = wb[\"Forecast\"]\n\n# 1. Clear the lock on the cells readers are meant to fill in.\nentry_fill = PatternFill(\"solid\", fgColor=\"FFF7E6\")\nfor row in ws.iter_rows(min_row=2, max_row=4, min_col=2, max_col=2):\n    for cell in row:\n        cell.protection = Protection(locked=False)\n        cell.fill = entry_fill\n\n# 2. Turn protection on. Now the still-locked cells refuse edits.\nws.protection.sheet = True\nws.protection.enable()\n\nwb.save(\"forecast_protected.xlsx\")\n",[186,689,690,701,712,716,729,744,748,754,776,826,838,857,867,871,876,886,891,895],{"__ignoreMap":184},[189,691,692,694,696,698],{"class":191,"line":192},[189,693,234],{"class":233},[189,695,238],{"class":237},[189,697,241],{"class":233},[189,699,700],{"class":237}," load_workbook\n",[189,702,703,705,707,709],{"class":191,"line":247},[189,704,234],{"class":233},[189,706,252],{"class":237},[189,708,241],{"class":233},[189,710,711],{"class":237}," Protection, PatternFill\n",[189,713,714],{"class":191,"line":260},[189,715,264],{"emptyLinePlaceholder":263},[189,717,718,720,722,725,727],{"class":191,"line":267},[189,719,270],{"class":237},[189,721,273],{"class":233},[189,723,724],{"class":237}," load_workbook(",[189,726,675],{"class":199},[189,728,514],{"class":237},[189,730,731,733,735,738,741],{"class":191,"line":279},[189,732,282],{"class":237},[189,734,273],{"class":233},[189,736,737],{"class":237}," wb[",[189,739,740],{"class":199},"\"Forecast\"",[189,742,743],{"class":237},"]\n",[189,745,746],{"class":191,"line":290},[189,747,264],{"emptyLinePlaceholder":263},[189,749,750],{"class":191,"line":301},[189,751,753],{"class":752},"s-wDw","# 1. Clear the lock on the cells readers are meant to fill in.\n",[189,755,756,759,761,763,765,767,769,771,774],{"class":191,"line":306},[189,757,758],{"class":237},"entry_fill ",[189,760,273],{"class":233},[189,762,525],{"class":237},[189,764,528],{"class":199},[189,766,315],{"class":237},[189,768,533],{"class":389},[189,770,273],{"class":233},[189,772,773],{"class":199},"\"FFF7E6\"",[189,775,514],{"class":237},[189,777,778,780,783,785,788,791,793,796,798,801,803,806,808,811,813,815,817,820,822,824],{"class":191,"line":334},[189,779,337],{"class":233},[189,781,782],{"class":237}," row ",[189,784,343],{"class":233},[189,786,787],{"class":237}," ws.iter_rows(",[189,789,790],{"class":389},"min_row",[189,792,273],{"class":233},[189,794,795],{"class":346},"2",[189,797,315],{"class":237},[189,799,800],{"class":389},"max_row",[189,802,273],{"class":233},[189,804,805],{"class":346},"4",[189,807,315],{"class":237},[189,809,810],{"class":389},"min_col",[189,812,273],{"class":233},[189,814,795],{"class":346},[189,816,315],{"class":237},[189,818,819],{"class":389},"max_col",[189,821,273],{"class":233},[189,823,795],{"class":346},[189,825,401],{"class":237},[189,827,828,831,833,835],{"class":191,"line":353},[189,829,830],{"class":233},"    for",[189,832,548],{"class":237},[189,834,343],{"class":233},[189,836,837],{"class":237}," row:\n",[189,839,840,843,845,848,850,852,855],{"class":191,"line":398},[189,841,842],{"class":237},"        cell.protection ",[189,844,273],{"class":233},[189,846,847],{"class":237}," Protection(",[189,849,209],{"class":389},[189,851,273],{"class":233},[189,853,854],{"class":346},"False",[189,856,514],{"class":237},[189,858,859,862,864],{"class":191,"line":404},[189,860,861],{"class":237},"        cell.fill ",[189,863,273],{"class":233},[189,865,866],{"class":237}," entry_fill\n",[189,868,869],{"class":191,"line":445},[189,870,264],{"emptyLinePlaceholder":263},[189,872,873],{"class":191,"line":450},[189,874,875],{"class":752},"# 2. Turn protection on. Now the still-locked cells refuse edits.\n",[189,877,878,881,883],{"class":191,"line":456},[189,879,880],{"class":237},"ws.protection.sheet ",[189,882,273],{"class":233},[189,884,885],{"class":346}," True\n",[189,887,888],{"class":191,"line":480},[189,889,890],{"class":237},"ws.protection.enable()\n",[189,892,893],{"class":191,"line":485},[189,894,264],{"emptyLinePlaceholder":263},[189,896,897,899,902],{"class":191,"line":517},[189,898,672],{"class":237},[189,900,901],{"class":199},"\"forecast_protected.xlsx\"",[189,903,514],{"class":237},[10,905,906,907,910],{},"Reversing the order does not work, and the failure is silent — ",[186,908,909],{},"enable()"," does not re-read the cells, but neither does it complain. What you get is a sheet where the protection is on and the unlock never registered, so nobody can type anywhere.",[10,912,913,914,917],{},"The tint is not decoration. On a protected sheet with no visual cue, a reader who clicks a locked cell sees a modal error with no explanation of where they ",[14,915,916],{},"should"," type. Two lines of fill saves the support message.",[10,919,920],{},"For a large or scattered input area, address it by range rather than by loop bounds:",[179,922,924],{"className":224,"code":923,"language":226,"meta":184,"style":184},"for row in ws[\"B2:B40\"]:\n    for cell in row:\n        cell.protection = Protection(locked=False)\n",[186,925,926,941,951],{"__ignoreMap":184},[189,927,928,930,932,934,936,939],{"class":191,"line":192},[189,929,337],{"class":233},[189,931,782],{"class":237},[189,933,343],{"class":233},[189,935,553],{"class":237},[189,937,938],{"class":199},"\"B2:B40\"",[189,940,559],{"class":237},[189,942,943,945,947,949],{"class":191,"line":247},[189,944,830],{"class":233},[189,946,548],{"class":237},[189,948,343],{"class":233},[189,950,837],{"class":237},[189,952,953,955,957,959,961,963,965],{"class":191,"line":260},[189,954,842],{"class":237},[189,956,273],{"class":233},[189,958,847],{"class":237},[189,960,209],{"class":389},[189,962,273],{"class":233},[189,964,854],{"class":346},[189,966,514],{"class":237},[174,968,970],{"id":969},"step-3-choose-what-stays-allowed","Step 3 — Choose what stays allowed",[25,972,35,978,35,981,35,984,35,987,35,991,35,999,35,1005,35,1009,35,1014,35,1019,35,1024,35,1027,35,1031,35,1036,35,1040],{"viewBox":973,"role":28,"ariaLabel":974,"ariaLabelledBy":975,"xmlns":33,"style":34},"0 0 800 224","The inverted protection flags: setting a flag to True blocks that operation while False permits it, which is the opposite of what the attribute names suggest.",[976,977],"flags-t","flags-d",[37,979,980],{"id":976},"Why protection flags read backwards",[41,982,983],{"id":977},"A note explaining that each protection attribute names the operation being protected, not the permission being granted. Setting autoFilter to True protects filtering, meaning readers cannot filter. Setting it to False leaves filtering unprotected, meaning readers can filter. Two example rows show the same flag producing opposite reader experiences, with the recommendation to set autoFilter and sort to False on any sheet carrying a table.",[45,985],{"x":47,"y":47,"width":48,"height":986,"fill":50},"224",[52,988,990],{"x":54,"y":989,"style":56},"28","the attribute names what is PROTECTED, not what is allowed",[45,992],{"x":103,"y":993,"width":994,"height":995,"rx":996,"fill":997,"stroke":998,"style":67},"44","356","76","12","#fee8f2","var(--accent,#f43f8f)",[52,1000,1004],{"x":1001,"y":1002,"style":1003},"208","70","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","autoFilter = True",[52,1006,1008],{"x":1001,"y":1007,"style":110},"94","filtering is protected",[52,1010,1013],{"x":1001,"y":1011,"style":1012},"112","font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","the reader CANNOT filter",[45,1015],{"x":1016,"y":993,"width":994,"height":995,"rx":996,"fill":1017,"stroke":1018,"style":67},"414","#d9f4f1","var(--teal,#0f9488)",[52,1020,1023],{"x":1021,"y":1002,"style":1022},"592","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","autoFilter = False",[52,1025,1026],{"x":1021,"y":1007,"style":110},"filtering is not protected",[52,1028,1030],{"x":1021,"y":1011,"style":1029},"font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","the reader CAN filter",[45,1032],{"x":103,"y":1033,"width":1034,"height":1035,"rx":996,"fill":77,"stroke":78,"style":67},"136","740","66",[52,1037,1039],{"x":54,"y":1038,"style":82},"162","on any sheet with a table: set autoFilter and sort to False",[52,1041,1043],{"x":54,"y":1042,"style":161},"186","the default protects both, so the filter dropdowns appear but silently do nothing",[10,1045,1046,1047],{},"Protection is a set of independent permissions, not a single switch. The naming is the thing to internalise: ",[1048,1049,1050,1051,1053,1054,1056],"strong",{},"each flag names what is protected, so ",[186,1052,501],{}," blocks and ",[186,1055,854],{}," permits.",[179,1058,1060],{"className":224,"code":1059,"language":226,"meta":184,"style":184},"ws.protection.sheet = True          # master switch: protection is ON\n\n# Keep the sheet usable.\nws.protection.autoFilter = False    # filtering allowed\nws.protection.sort = False          # sorting allowed\nws.protection.selectLockedCells = False   # locked cells still selectable\n\n# Block the destructive operations.\nws.protection.formatCells = True\nws.protection.insertRows = True\nws.protection.insertColumns = True\nws.protection.deleteRows = True\nws.protection.deleteColumns = True\n\nws.protection.enable()\n",[186,1061,1062,1074,1078,1083,1096,1108,1120,1124,1129,1138,1147,1156,1165,1174,1178],{"__ignoreMap":184},[189,1063,1064,1066,1068,1071],{"class":191,"line":192},[189,1065,880],{"class":237},[189,1067,273],{"class":233},[189,1069,1070],{"class":346}," True",[189,1072,1073],{"class":752},"          # master switch: protection is ON\n",[189,1075,1076],{"class":191,"line":247},[189,1077,264],{"emptyLinePlaceholder":263},[189,1079,1080],{"class":191,"line":260},[189,1081,1082],{"class":752},"# Keep the sheet usable.\n",[189,1084,1085,1088,1090,1093],{"class":191,"line":267},[189,1086,1087],{"class":237},"ws.protection.autoFilter ",[189,1089,273],{"class":233},[189,1091,1092],{"class":346}," False",[189,1094,1095],{"class":752},"    # filtering allowed\n",[189,1097,1098,1101,1103,1105],{"class":191,"line":279},[189,1099,1100],{"class":237},"ws.protection.sort ",[189,1102,273],{"class":233},[189,1104,1092],{"class":346},[189,1106,1107],{"class":752},"          # sorting allowed\n",[189,1109,1110,1113,1115,1117],{"class":191,"line":290},[189,1111,1112],{"class":237},"ws.protection.selectLockedCells ",[189,1114,273],{"class":233},[189,1116,1092],{"class":346},[189,1118,1119],{"class":752},"   # locked cells still selectable\n",[189,1121,1122],{"class":191,"line":301},[189,1123,264],{"emptyLinePlaceholder":263},[189,1125,1126],{"class":191,"line":306},[189,1127,1128],{"class":752},"# Block the destructive operations.\n",[189,1130,1131,1134,1136],{"class":191,"line":334},[189,1132,1133],{"class":237},"ws.protection.formatCells ",[189,1135,273],{"class":233},[189,1137,885],{"class":346},[189,1139,1140,1143,1145],{"class":191,"line":353},[189,1141,1142],{"class":237},"ws.protection.insertRows ",[189,1144,273],{"class":233},[189,1146,885],{"class":346},[189,1148,1149,1152,1154],{"class":191,"line":398},[189,1150,1151],{"class":237},"ws.protection.insertColumns ",[189,1153,273],{"class":233},[189,1155,885],{"class":346},[189,1157,1158,1161,1163],{"class":191,"line":404},[189,1159,1160],{"class":237},"ws.protection.deleteRows ",[189,1162,273],{"class":233},[189,1164,885],{"class":346},[189,1166,1167,1170,1172],{"class":191,"line":445},[189,1168,1169],{"class":237},"ws.protection.deleteColumns ",[189,1171,273],{"class":233},[189,1173,885],{"class":346},[189,1175,1176],{"class":191,"line":450},[189,1177,264],{"emptyLinePlaceholder":263},[189,1179,1180],{"class":191,"line":456},[189,1181,890],{"class":237},[10,1183,1184,1185,1188,1189,1192,1193,1197],{},"The two worth setting on every report are ",[186,1186,1187],{},"autoFilter"," and ",[186,1190,1191],{},"sort",". A protected sheet defaults to blocking both, which quietly disables the filter dropdowns added in ",[19,1194,1196],{"href":1195},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002F","creating Excel tables and autofilters with Python"," — the table looks normal and the controls simply do nothing.",[1199,1200,1201,1222],"table",{},[1202,1203,1204],"thead",{},[1205,1206,1207,1211,1216],"tr",{},[1208,1209,1210],"th",{},"Flag",[1208,1212,1213,1215],{},[186,1214,501],{}," means",[1208,1217,1218,1219,1221],{},"Set to ",[186,1220,854],{}," when",[1223,1224,1225,1238,1250,1265,1278,1292],"tbody",{},[1205,1226,1227,1232,1235],{},[1228,1229,1230],"td",{},[186,1231,1187],{},[1228,1233,1234],{},"filtering blocked",[1228,1236,1237],{},"the sheet has a filter or table",[1205,1239,1240,1244,1247],{},[1228,1241,1242],{},[186,1243,1191],{},[1228,1245,1246],{},"sorting blocked",[1228,1248,1249],{},"readers browse the data",[1205,1251,1252,1257,1260],{},[1228,1253,1254],{},[186,1255,1256],{},"formatCells",[1228,1258,1259],{},"restyling blocked",[1228,1261,1262,1263],{},"almost always leave ",[186,1264,501],{},[1205,1266,1267,1272,1275],{},[1228,1268,1269],{},[186,1270,1271],{},"insertRows",[1228,1273,1274],{},"inserting blocked",[1228,1276,1277],{},"readers append their own rows",[1205,1279,1280,1285,1288],{},[1228,1281,1282],{},[186,1283,1284],{},"deleteRows",[1228,1286,1287],{},"deleting blocked",[1228,1289,1262,1290],{},[186,1291,501],{},[1205,1293,1294,1299,1302],{},[1228,1295,1296],{},[186,1297,1298],{},"selectLockedCells",[1228,1300,1301],{},"locked cells unselectable",[1228,1303,1304],{},"you want values still copyable",[10,1306,1307,1308,1311,1312,1314],{},"Leaving ",[186,1309,1310],{},"selectLockedCells = False"," matters more than it sounds: with it set to ",[186,1313,501],{},", readers cannot even click a locked cell to copy its value, which makes a report feel broken.",[174,1316,1318],{"id":1317},"step-4-protect-only-part-of-a-sheet","Step 4 — Protect only part of a sheet",[10,1320,1321,1322,1325],{},"Sometimes one group should edit a block that everyone else cannot. openpyxl supports Excel's \"allow users to edit ranges\" through ",[186,1323,1324],{},"ws.protection.add",":",[179,1327,1329],{"className":224,"code":1328,"language":226,"meta":184,"style":184},"from openpyxl import load_workbook\nfrom openpyxl.worksheet.protection import SheetProtection\nfrom openpyxl.workbook.protection import WorkbookProtection\n\nwb = load_workbook(\"forecast.xlsx\")\nws = wb[\"Forecast\"]\n\nws.protection.sheet = True\nws.protection.password = \"report\"\nws.protection.enable()\n\n# A named range that a specific team can unlock with their own password.\nws.protection.add(\n    name=\"ForecastInputs\",\n    sqref=\"B2:B4\",\n    password=\"planning\",\n)\n\nwb.save(\"forecast_partial.xlsx\")\n",[186,1330,1331,1341,1353,1365,1369,1381,1393,1397,1405,1415,1419,1423,1428,1433,1446,1458,1470,1474,1478],{"__ignoreMap":184},[189,1332,1333,1335,1337,1339],{"class":191,"line":192},[189,1334,234],{"class":233},[189,1336,238],{"class":237},[189,1338,241],{"class":233},[189,1340,700],{"class":237},[189,1342,1343,1345,1348,1350],{"class":191,"line":247},[189,1344,234],{"class":233},[189,1346,1347],{"class":237}," openpyxl.worksheet.protection ",[189,1349,241],{"class":233},[189,1351,1352],{"class":237}," SheetProtection\n",[189,1354,1355,1357,1360,1362],{"class":191,"line":260},[189,1356,234],{"class":233},[189,1358,1359],{"class":237}," openpyxl.workbook.protection ",[189,1361,241],{"class":233},[189,1363,1364],{"class":237}," WorkbookProtection\n",[189,1366,1367],{"class":191,"line":267},[189,1368,264],{"emptyLinePlaceholder":263},[189,1370,1371,1373,1375,1377,1379],{"class":191,"line":279},[189,1372,270],{"class":237},[189,1374,273],{"class":233},[189,1376,724],{"class":237},[189,1378,675],{"class":199},[189,1380,514],{"class":237},[189,1382,1383,1385,1387,1389,1391],{"class":191,"line":290},[189,1384,282],{"class":237},[189,1386,273],{"class":233},[189,1388,737],{"class":237},[189,1390,740],{"class":199},[189,1392,743],{"class":237},[189,1394,1395],{"class":191,"line":301},[189,1396,264],{"emptyLinePlaceholder":263},[189,1398,1399,1401,1403],{"class":191,"line":306},[189,1400,880],{"class":237},[189,1402,273],{"class":233},[189,1404,885],{"class":346},[189,1406,1407,1410,1412],{"class":191,"line":334},[189,1408,1409],{"class":237},"ws.protection.password ",[189,1411,273],{"class":233},[189,1413,1414],{"class":199}," \"report\"\n",[189,1416,1417],{"class":191,"line":353},[189,1418,890],{"class":237},[189,1420,1421],{"class":191,"line":398},[189,1422,264],{"emptyLinePlaceholder":263},[189,1424,1425],{"class":191,"line":404},[189,1426,1427],{"class":752},"# A named range that a specific team can unlock with their own password.\n",[189,1429,1430],{"class":191,"line":445},[189,1431,1432],{"class":237},"ws.protection.add(\n",[189,1434,1435,1438,1440,1443],{"class":191,"line":450},[189,1436,1437],{"class":389},"    name",[189,1439,273],{"class":233},[189,1441,1442],{"class":199},"\"ForecastInputs\"",[189,1444,1445],{"class":237},",\n",[189,1447,1448,1451,1453,1456],{"class":191,"line":456},[189,1449,1450],{"class":389},"    sqref",[189,1452,273],{"class":233},[189,1454,1455],{"class":199},"\"B2:B4\"",[189,1457,1445],{"class":237},[189,1459,1460,1463,1465,1468],{"class":191,"line":480},[189,1461,1462],{"class":389},"    password",[189,1464,273],{"class":233},[189,1466,1467],{"class":199},"\"planning\"",[189,1469,1445],{"class":237},[189,1471,1472],{"class":191,"line":485},[189,1473,514],{"class":237},[189,1475,1476],{"class":191,"line":517},[189,1477,264],{"emptyLinePlaceholder":263},[189,1479,1480,1482,1485],{"class":191,"line":543},[189,1481,672],{"class":237},[189,1483,1484],{"class":199},"\"forecast_partial.xlsx\"",[189,1486,514],{"class":237},[10,1488,1489,1490,1493],{},"In practice, the simpler unlock-the-range approach covers most needs and does not require anyone to remember a second password. Reach for ",[186,1491,1492],{},"add"," only when genuinely different groups need different access to the same sheet.",[174,1495,1497],{"id":1496},"step-5-verify-before-shipping","Step 5 — Verify before shipping",[10,1499,1500],{},"Protection is state that a later stage can silently discard, so assert on it. This is a cheap test and it catches the most common regression in a reporting pipeline:",[25,1502,35,1508,35,1511,35,1514,35,1517,35,1520,35,1526,35,1530,35,1533,35,1537,35,1540,35,1543,35,1547,35,1550,35,1553,35,1556,35,1559,35,1562,35,1567,35,1570,35,1573,35,1576],{"viewBox":1503,"role":28,"ariaLabel":1504,"ariaLabelledBy":1505,"xmlns":33,"style":34},"0 0 800 226","Three assertions on a finished workbook: sheet protection is enabled, the intended input range is unlocked, and the formula column is still locked.",[1506,1507],"assert-t","assert-d",[37,1509,1510],{"id":1506},"The three checks that prove a report is protected as intended",[41,1512,1513],{"id":1507},"Three assertions run against the saved workbook. The first confirms the sheet protection flag is set at all. The second confirms every cell in the intended input range has its locked attribute cleared, so readers can type. The third confirms the formula column is still locked, which catches an over-broad unlock. Together they detect both a missing protection step and one that unlocked too much.",[45,1515],{"x":47,"y":47,"width":48,"height":1516,"fill":50},"226",[45,1518],{"x":647,"y":989,"width":1519,"height":95,"rx":647,"fill":65,"stroke":66,"style":67},"248",[52,1521,1525],{"x":1522,"y":1523,"style":1524},"138","58","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","1 · protection is on",[52,1527,1529],{"x":1522,"y":1528,"style":110},"90","ws.protection.sheet",[52,1531,1532],{"x":1522,"y":1011,"style":110},"is True",[52,1534,1536],{"x":1522,"y":1535,"style":161},"144","catches a later to_excel",[52,1538,1539],{"x":1522,"y":87,"style":161},"that replaced the sheet",[45,1541],{"x":1542,"y":989,"width":1519,"height":95,"rx":647,"fill":77,"stroke":78,"style":67},"276",[52,1544,1546],{"x":54,"y":1523,"style":1545},"font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","2 · inputs are unlocked",[52,1548,1549],{"x":54,"y":1528,"style":110},"every cell in B2:B4",[52,1551,1552],{"x":54,"y":1011,"style":110},"has locked False",[52,1554,1555],{"x":54,"y":1535,"style":161},"catches the ordering bug",[52,1557,1558],{"x":54,"y":87,"style":161},"that locks the whole sheet",[45,1560],{"x":1561,"y":989,"width":1519,"height":95,"rx":647,"fill":1017,"stroke":1018,"style":67},"538",[52,1563,1566],{"x":1564,"y":1523,"style":1565},"662","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","3 · formulas still locked",[52,1568,1569],{"x":1564,"y":1528,"style":110},"column D remains",[52,1571,1572],{"x":1564,"y":1011,"style":110},"locked True",[52,1574,1575],{"x":1564,"y":1535,"style":161},"catches an unlock range",[52,1577,1578],{"x":1564,"y":87,"style":161},"that was too wide",[179,1580,1582],{"className":224,"code":1581,"language":226,"meta":184,"style":184},"from openpyxl import load_workbook\n\ndef assert_protected(path, sheet, editable=\"B2:B4\", locked_col=\"D\"):\n    wb = load_workbook(path)\n    ws = wb[sheet]\n\n    assert ws.protection.sheet, f\"{sheet} is not protected\"\n\n    for row in ws[editable]:\n        for cell in row:\n            assert cell.protection.locked is False, \\\n                f\"{cell.coordinate} should be unlocked\"\n\n    for (cell,) in ws.iter_rows(min_row=2, max_row=4,\n                                min_col=4, max_col=4):\n        assert cell.protection.locked is not False, \\\n            f\"{cell.coordinate} in the formula column is unlocked\"\n\n    return True\n\nassert_protected(\"forecast_protected.xlsx\", \"Forecast\")\nprint(\"protection verified\")\n",[186,1583,1584,1594,1598,1624,1634,1644,1648,1670,1674,1685,1696,1712,1729,1733,1760,1779,1795,1811,1815,1822,1826,1839],{"__ignoreMap":184},[189,1585,1586,1588,1590,1592],{"class":191,"line":192},[189,1587,234],{"class":233},[189,1589,238],{"class":237},[189,1591,241],{"class":233},[189,1593,700],{"class":237},[189,1595,1596],{"class":191,"line":247},[189,1597,264],{"emptyLinePlaceholder":263},[189,1599,1600,1603,1607,1610,1612,1614,1617,1619,1622],{"class":191,"line":260},[189,1601,1602],{"class":233},"def",[189,1604,1606],{"class":1605},"s_Opv"," assert_protected",[189,1608,1609],{"class":237},"(path, sheet, editable",[189,1611,273],{"class":233},[189,1613,1455],{"class":199},[189,1615,1616],{"class":237},", locked_col",[189,1618,273],{"class":233},[189,1620,1621],{"class":199},"\"D\"",[189,1623,401],{"class":237},[189,1625,1626,1629,1631],{"class":191,"line":267},[189,1627,1628],{"class":237},"    wb ",[189,1630,273],{"class":233},[189,1632,1633],{"class":237}," load_workbook(path)\n",[189,1635,1636,1639,1641],{"class":191,"line":279},[189,1637,1638],{"class":237},"    ws ",[189,1640,273],{"class":233},[189,1642,1643],{"class":237}," wb[sheet]\n",[189,1645,1646],{"class":191,"line":290},[189,1647,264],{"emptyLinePlaceholder":263},[189,1649,1650,1653,1656,1658,1660,1662,1665,1667],{"class":191,"line":301},[189,1651,1652],{"class":233},"    assert",[189,1654,1655],{"class":237}," ws.protection.sheet, ",[189,1657,415],{"class":233},[189,1659,440],{"class":199},[189,1661,422],{"class":421},[189,1663,1664],{"class":237},"sheet",[189,1666,428],{"class":421},[189,1668,1669],{"class":199}," is not protected\"\n",[189,1671,1672],{"class":191,"line":306},[189,1673,264],{"emptyLinePlaceholder":263},[189,1675,1676,1678,1680,1682],{"class":191,"line":334},[189,1677,830],{"class":233},[189,1679,782],{"class":237},[189,1681,343],{"class":233},[189,1683,1684],{"class":237}," ws[editable]:\n",[189,1686,1687,1690,1692,1694],{"class":191,"line":353},[189,1688,1689],{"class":233},"        for",[189,1691,548],{"class":237},[189,1693,343],{"class":233},[189,1695,837],{"class":237},[189,1697,1698,1701,1704,1707,1709],{"class":191,"line":398},[189,1699,1700],{"class":233},"            assert",[189,1702,1703],{"class":237}," cell.protection.locked ",[189,1705,1706],{"class":233},"is",[189,1708,1092],{"class":346},[189,1710,1711],{"class":237},", \\\n",[189,1713,1714,1717,1719,1721,1724,1726],{"class":191,"line":404},[189,1715,1716],{"class":233},"                f",[189,1718,440],{"class":199},[189,1720,422],{"class":421},[189,1722,1723],{"class":237},"cell.coordinate",[189,1725,428],{"class":421},[189,1727,1728],{"class":199}," should be unlocked\"\n",[189,1730,1731],{"class":191,"line":445},[189,1732,264],{"emptyLinePlaceholder":263},[189,1734,1735,1737,1740,1742,1744,1746,1748,1750,1752,1754,1756,1758],{"class":191,"line":450},[189,1736,830],{"class":233},[189,1738,1739],{"class":237}," (cell,) ",[189,1741,343],{"class":233},[189,1743,787],{"class":237},[189,1745,790],{"class":389},[189,1747,273],{"class":233},[189,1749,795],{"class":346},[189,1751,315],{"class":237},[189,1753,800],{"class":389},[189,1755,273],{"class":233},[189,1757,805],{"class":346},[189,1759,1445],{"class":237},[189,1761,1762,1765,1767,1769,1771,1773,1775,1777],{"class":191,"line":456},[189,1763,1764],{"class":389},"                                min_col",[189,1766,273],{"class":233},[189,1768,805],{"class":346},[189,1770,315],{"class":237},[189,1772,819],{"class":389},[189,1774,273],{"class":233},[189,1776,805],{"class":346},[189,1778,401],{"class":237},[189,1780,1781,1784,1786,1788,1791,1793],{"class":191,"line":480},[189,1782,1783],{"class":233},"        assert",[189,1785,1703],{"class":237},[189,1787,1706],{"class":233},[189,1789,1790],{"class":233}," not",[189,1792,1092],{"class":346},[189,1794,1711],{"class":237},[189,1796,1797,1800,1802,1804,1806,1808],{"class":191,"line":485},[189,1798,1799],{"class":233},"            f",[189,1801,440],{"class":199},[189,1803,422],{"class":421},[189,1805,1723],{"class":237},[189,1807,428],{"class":421},[189,1809,1810],{"class":199}," in the formula column is unlocked\"\n",[189,1812,1813],{"class":191,"line":517},[189,1814,264],{"emptyLinePlaceholder":263},[189,1816,1817,1820],{"class":191,"line":543},[189,1818,1819],{"class":233},"    return",[189,1821,885],{"class":346},[189,1823,1824],{"class":191,"line":562},[189,1825,264],{"emptyLinePlaceholder":263},[189,1827,1828,1831,1833,1835,1837],{"class":191,"line":573},[189,1829,1830],{"class":237},"assert_protected(",[189,1832,901],{"class":199},[189,1834,315],{"class":237},[189,1836,740],{"class":199},[189,1838,514],{"class":237},[189,1840,1841,1844,1846,1849],{"class":191,"line":584},[189,1842,1843],{"class":346},"print",[189,1845,623],{"class":237},[189,1847,1848],{"class":199},"\"protection verified\"",[189,1850,514],{"class":237},[10,1852,1853,1854,1857,1858,1860,1861,1863,1864,1866,1867,23],{},"Note ",[186,1855,1856],{},"is not False"," rather than ",[186,1859,1532],{}," in the third check: cells that were never touched carry the inherited default rather than an explicit ",[186,1862,501],{},", so a strict identity test against ",[186,1865,501],{}," gives a false failure. Drop this into the suite described in ",[19,1868,1870],{"href":1869},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002F","testing Excel output with pytest",[174,1872,1874],{"id":1873},"common-pitfalls-and-fixes","Common pitfalls and fixes",[1199,1876,1877,1890],{},[1202,1878,1879],{},[1205,1880,1881,1884,1887],{},[1208,1882,1883],{},"Symptom",[1208,1885,1886],{},"Cause",[1208,1888,1889],{},"Fix",[1223,1891,1892,1907,1926,1941,1956,1971,1989,2003],{},[1205,1893,1894,1897,1900],{},[1228,1895,1896],{},"Nothing is editable",[1228,1898,1899],{},"Protection enabled without unlocking any cells",[1228,1901,1902,1903,1906],{},"Assign ",[186,1904,1905],{},"Protection(locked=False)"," to the input range first.",[1205,1908,1909,1912,1917],{},[1228,1910,1911],{},"Everything is still editable",[1228,1913,1914,1916],{},[186,1915,1529],{}," never set",[1228,1918,1919,1920,1923,1924,23],{},"Set ",[186,1921,1922],{},"sheet = True"," and call ",[186,1925,909],{},[1205,1927,1928,1931,1936],{},[1228,1929,1930],{},"Filter dropdowns do nothing",[1228,1932,1933,1935],{},[186,1934,1187],{}," defaults to blocked",[1228,1937,1938,23],{},[186,1939,1940],{},"ws.protection.autoFilter = False",[1205,1942,1943,1946,1951],{},[1228,1944,1945],{},"Readers cannot copy values",[1228,1947,1948],{},[186,1949,1950],{},"selectLockedCells = True",[1228,1952,1953,1954,23],{},"Set it to ",[186,1955,854],{},[1205,1957,1958,1961,1968],{},[1228,1959,1960],{},"Protection gone after the job runs",[1228,1962,1963,1964,1967],{},"A later ",[186,1965,1966],{},"to_excel"," replaced the sheet",[1228,1969,1970],{},"Protect as the final step.",[1205,1972,1973,1976,1979],{},[1228,1974,1975],{},"Unlocking a whole column locked nothing",[1228,1977,1978],{},"Assigned to the column dimension, not the cells",[1228,1980,1981,1982,1985,1986,23],{},"Iterate the cells; ",[186,1983,1984],{},"column_dimensions"," has no ",[186,1987,1988],{},"protection",[1205,1990,1991,1994,2000],{},[1228,1992,1993],{},"Password prompt never appears",[1228,1995,1996,1997,1999],{},"Password set but ",[186,1998,1664],{}," not enabled",[1228,2001,2002],{},"Both are required.",[1205,2004,2005,2008,2015],{},[1228,2006,2007],{},"Formatting lost after protecting",[1228,2009,2010,2011,2014],{},"Reassigned ",[186,2012,2013],{},"cell.protection"," on a styled cell",[1228,2016,2017,2020,2021,23],{},[186,2018,2019],{},"Protection"," replaces only the protection part; check you did not also reassign ",[186,2022,2023],{},"cell.style",[174,2025,2027],{"id":2026},"performance-and-scale-notes","Performance and scale notes",[10,2029,2030,2031,2033,2034,2036],{},"Assigning ",[186,2032,2013],{}," creates a style entry, and openpyxl deduplicates identical ones — so unlocking a thousand cells with the same ",[186,2035,1905],{}," object costs one style, not a thousand. Create the object once outside the loop rather than constructing a new one per cell:",[179,2038,2040],{"className":224,"code":2039,"language":226,"meta":184,"style":184},"from openpyxl.styles import Protection\n\nunlocked = Protection(locked=False)          # one object, reused\nfor row in ws.iter_rows(min_row=2, max_row=50_000, min_col=2, max_col=2):\n    for cell in row:\n        cell.protection = unlocked\n",[186,2041,2042,2053,2057,2078,2121,2131],{"__ignoreMap":184},[189,2043,2044,2046,2048,2050],{"class":191,"line":192},[189,2045,234],{"class":233},[189,2047,252],{"class":237},[189,2049,241],{"class":233},[189,2051,2052],{"class":237}," Protection\n",[189,2054,2055],{"class":191,"line":247},[189,2056,264],{"emptyLinePlaceholder":263},[189,2058,2059,2062,2064,2066,2068,2070,2072,2075],{"class":191,"line":260},[189,2060,2061],{"class":237},"unlocked ",[189,2063,273],{"class":233},[189,2065,847],{"class":237},[189,2067,209],{"class":389},[189,2069,273],{"class":233},[189,2071,854],{"class":346},[189,2073,2074],{"class":237},")          ",[189,2076,2077],{"class":752},"# one object, reused\n",[189,2079,2080,2082,2084,2086,2088,2090,2092,2094,2096,2098,2100,2103,2105,2107,2109,2111,2113,2115,2117,2119],{"class":191,"line":267},[189,2081,337],{"class":233},[189,2083,782],{"class":237},[189,2085,343],{"class":233},[189,2087,787],{"class":237},[189,2089,790],{"class":389},[189,2091,273],{"class":233},[189,2093,795],{"class":346},[189,2095,315],{"class":237},[189,2097,800],{"class":389},[189,2099,273],{"class":233},[189,2101,2102],{"class":346},"50_000",[189,2104,315],{"class":237},[189,2106,810],{"class":389},[189,2108,273],{"class":233},[189,2110,795],{"class":346},[189,2112,315],{"class":237},[189,2114,819],{"class":389},[189,2116,273],{"class":233},[189,2118,795],{"class":346},[189,2120,401],{"class":237},[189,2122,2123,2125,2127,2129],{"class":191,"line":279},[189,2124,830],{"class":233},[189,2126,548],{"class":237},[189,2128,343],{"class":233},[189,2130,837],{"class":237},[189,2132,2133,2135,2137],{"class":191,"line":290},[189,2134,842],{"class":237},[189,2136,273],{"class":233},[189,2138,2139],{"class":237}," unlocked\n",[10,2141,2142],{},"Constructing inside the loop still deduplicates on save, but it allocates fifty thousand short-lived objects along the way, which is measurable on large sheets.",[10,2144,2145,2146,2149,2150,2152],{},"Two structural notes for big reports. Protection cannot be used with openpyxl's ",[186,2147,2148],{},"write_only"," mode — that mode streams rows and never holds cells to style — so a workbook that needs both bulk volume and protection has to be written in normal mode, or written streaming and then re-opened to protect. And Excel's ceiling of roughly 64,000 distinct cell formats counts protection variants too, which is another reason to reuse one shared ",[186,2151,2019],{}," object rather than creating them ad hoc.",[10,2154,2155,2156,23],{},"Where a report is genuinely large, the pragmatic split is the same one used for macro workbooks: keep the protected, formula-bearing summary small, and push bulk detail to a separate sheet or file written with the streaming approach in ",[19,2157,2159],{"href":2158},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fwrite-large-dataframes-to-excel-with-write-only-mode\u002F","writing large DataFrames with write-only mode",[174,2161,2163],{"id":2162},"conclusion","Conclusion",[10,2165,2166,2167,2169,2170,1923,2173,2175,2176,2179,2180,1188,2182,2184,2185,2187],{},"Sheet protection in openpyxl is two steps in a fixed order: clear ",[186,2168,209],{}," on the cells readers should be able to edit, then set ",[186,2171,2172],{},"ws.protection.sheet = True",[186,2174,909],{},". Tint the input range so it is obvious where typing is expected. Remember the flags name what is ",[14,2177,2178],{},"protected",", so set ",[186,2181,1187],{},[186,2183,1191],{}," to ",[186,2186,854],{}," to keep the sheet browsable. Protect last, after all data is written, and assert on the result — a protection step that silently stopped working looks exactly like one that is fine.",[174,2189,2191],{"id":2190},"frequently-asked-questions","Frequently asked questions",[10,2193,2194,2197,2198,2201,2202,2204],{},[1048,2195,2196],{},"Why is my whole sheet read-only after protecting it?","\nEvery cell starts with ",[186,2199,2200],{},"locked=True",", so enabling protection locks all of them. Clear the flag on the cells readers should be able to type into by assigning ",[186,2203,1905],{}," before you enable protection.",[10,2206,2207,2210],{},[1048,2208,2209],{},"Do I need a password on sheet protection?","\nNot for the guard-rail effect — protection works with no password at all and still prevents accidental edits. A password only stops a reader clicking Unprotect Sheet, and it is easily removed, so treat it as a speed bump.",[10,2212,2213,2219,2220,1053,2222,2224,2225,1188,2227,2230],{},[1048,2214,2215,2216,2218],{},"Why did ",[186,2217,1187],{}," stop working after protection?","\nThe flags name what is protected, so ",[186,2221,501],{},[186,2223,854],{}," permits. Set ",[186,2226,1940],{},[186,2228,2229],{},"ws.protection.sort = False"," to keep filtering and sorting available on a protected sheet.",[10,2232,2233,2236,2237,2239],{},[1048,2234,2235],{},"Can I protect only part of a sheet?","\nYes, in two ways. Leave the editable range unlocked, which is the usual approach, or add an unprotected range with its own password through ",[186,2238,1324],{}," so a specific group can edit one block.",[10,2241,2242,2245,2246,2248],{},[1048,2243,2244],{},"Does protection survive a pandas write?","\nNo. ",[186,2247,1966],{}," replaces the whole sheet, discarding protection along with comments and column widths. Apply protection as the final step, after all data has been written.",[174,2250,2252],{"id":2251},"related","Related",[2254,2255,2256,2263,2270,2277,2284],"ul",{},[2257,2258,2259,2260,2262],"li",{},"Up to the parent: ",[19,2261,22],{"href":21}," — how sheet protection fits with the other layers.",[2257,2264,2265,2269],{},[19,2266,2268],{"href":2267},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fpassword-protect-an-excel-file-with-python\u002F","Password Protect an Excel File with Python"," — real encryption, for when reading must be stopped too.",[2257,2271,2272,2276],{},[19,2273,2275],{"href":2274},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fhide-sheets-rows-and-columns-with-openpyxl\u002F","Hide Sheets, Rows and Columns with openpyxl"," — tidying the working sheets before you protect.",[2257,2278,2279,2283],{},[19,2280,2282],{"href":2281},"\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"," — constraining what the unlocked cells accept.",[2257,2285,2286,2290],{},[19,2287,2289],{"href":2288},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002F","Styling Excel Cells with openpyxl"," — the fills used to signal the input range.",[2292,2293,2294],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":184,"searchDepth":247,"depth":247,"links":2296},[2297,2298,2299,2300,2301,2302,2303,2304,2305,2306,2307],{"id":176,"depth":247,"text":177},{"id":217,"depth":247,"text":218},{"id":680,"depth":247,"text":681},{"id":969,"depth":247,"text":970},{"id":1317,"depth":247,"text":1318},{"id":1496,"depth":247,"text":1497},{"id":1873,"depth":247,"text":1874},{"id":2026,"depth":247,"text":2027},{"id":2162,"depth":247,"text":2163},{"id":2190,"depth":247,"text":2191},{"id":2251,"depth":247,"text":2252},"2026-08-15","Stop readers overwriting formulas in a generated report: unlock the input cells, enable sheet protection, choose which operations stay allowed, and verify it on the way out.","md",[2312,2314,2316,2319,2321],{"q":2196,"a":2313},"Every cell starts with locked=True, so enabling protection locks all of them. Clear the flag on the cells readers should be able to type into by assigning Protection(locked=False) before you enable protection.",{"q":2209,"a":2315},"Not for the guard-rail effect — protection works with no password at all and still prevents accidental edits. A password only stops a reader clicking Unprotect Sheet, and it is easily removed, so treat it as a speed bump.",{"q":2317,"a":2318},"Why did autoFilter stop working after protection?","The flags name what is protected, so True blocks and False permits. Set ws.protection.autoFilter = False and ws.protection.sort = False to keep filtering and sorting available on a protected sheet.",{"q":2235,"a":2320},"Yes, in two ways. Leave the editable range unlocked, which is the usual approach, or add an unprotected range with its own password through ws.protection.add so a specific group can edit one block.",{"q":2244,"a":2322},"No. to_excel replaces the whole sheet, discarding protection along with comments and column widths. Apply protection as the final step, after all data has been written.",{},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Flock-cells-and-protect-a-sheet-with-openpyxl",{"title":2326,"description":2327},"Lock Cells and Protect a Sheet with openpyxl (Python)","The unlock-then-protect pattern in openpyxl: cell Protection objects, the inverted permission flags, unprotected ranges, and testing that the output really is protected.","lock-cells-and-protect-a-sheet-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Flock-cells-and-protect-a-sheet-with-openpyxl\u002Findex","how-to","vtcuyd25Of33TmR2PcClRxD9tuV4VXz96Niio6udh94",[2333,2336],{"title":2275,"path":2334,"stem":2335,"children":-1},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fhide-sheets-rows-and-columns-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fhide-sheets-rows-and-columns-with-openpyxl\u002Findex",{"title":2268,"path":2337,"stem":2338,"children":-1},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fpassword-protect-an-excel-file-with-python","formatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fpassword-protect-an-excel-file-with-python\u002Findex",1786800027185]