[{"data":1,"prerenderedAt":3406},["ShallowReactive",2],{"doc:\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks":3,"surround:\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks":3398},{"id":4,"title":5,"body":6,"dateModified":3372,"datePublished":3372,"description":3373,"extension":3374,"faq":3375,"meta":3389,"navigation":428,"path":3390,"seo":3391,"slug":3394,"stem":3395,"type":3396,"__hash__":3397},"docs\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Findex.md","Protecting and Sharing Excel Workbooks",{"type":7,"value":8,"toc":3358},"minimark",[9,19,153,158,161,168,174,177,180,248,252,259,382,965,968,971,1042,1071,1078,1082,1089,1162,1170,1174,1177,1262,1268,1271,1445,1452,1456,1459,1478,1713,1716,1849,1856,1860,1863,2007,2014,2017,2170,2174,2177,2238,2241,2244,2519,2527,2531,2534,2645,2652,2655,2807,2815,2819,2822,3183,3190,3194,3256,3260,3266,3272,3278,3291,3297,3303,3307,3354],[10,11,12,13,18],"p",{},"A generated report rarely goes to somebody who should change it. They should read the totals, filter the detail, maybe type into three input cells — and not overwrite the formula column by accident. Excel has a layered set of controls for exactly this, and Python can set nearly all of them at write time: locking cells, protecting sheets and workbook structure, hiding working sheets, annotating cells with comments, and encrypting the file itself. This page maps the layers, explains which ones are genuine security and which are guard rails, and shows the code for each. It sits within ",[14,15,17],"a",{"href":16},"\u002Fformatting-and-charting-excel-reports-with-python\u002F","Formatting and Charting Excel Reports with Python",".",[20,21,30,31,30,35,30,39,30,46,30,53,30,63,30,69,30,74,30,81,30,87,30,92,30,97,30,101,30,104,30,109,30,114,30,119,30,123,30,126,30,130,30,136,30,141,30,145,30,148],"svg",{"viewBox":22,"role":23,"ariaLabel":24,"ariaLabelledBy":25,"xmlns":28,"style":29},"0 0 800 300","img","Four protection layers ranked by strength: file encryption is real security, workbook structure protection and sheet protection are interface guards, and hidden sheets are only obscurity.",[26,27],"layers-t","layers-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  ",[32,33,34],"title",{"id":26},"The four protection layers, and which of them is real security",[36,37,38],"desc",{"id":27},"Four stacked bands. File encryption sits at the top as the only genuine confidentiality control, since the bytes cannot be read without the password. Workbook structure protection stops sheets being added, deleted or renamed. Sheet protection stops cell edits in Excel's interface. Hidden and very hidden sheets are obscurity only. The lower three are all trivially removed by any library that can open the file, so they guard against accident rather than intent.",[40,41],"rect",{"x":42,"y":42,"width":43,"height":44,"fill":45},"0","800","300","#ffffff",[47,48,52],"text",{"x":49,"y":50,"style":51},"400","26","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","strongest at the top",[40,54],{"x":55,"y":56,"width":57,"height":58,"rx":59,"fill":60,"stroke":61,"style":62},"60","38","680","56","12","#d9f4f1","var(--teal,#0f9488)","stroke-width:2px",[47,64,68],{"x":65,"y":66,"style":67},"90","62","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157)","file encryption",[47,70,73],{"x":65,"y":71,"style":72},"82","font-size:11px;fill:var(--text,#172033)","the bytes are unreadable without the password — genuine confidentiality",[40,75],{"x":76,"y":77,"width":78,"height":79,"rx":80,"fill":45,"stroke":61},"600","50","124","32","8",[47,82,86],{"x":83,"y":84,"style":85},"662","71","font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","real security",[40,88],{"x":55,"y":89,"width":57,"height":58,"rx":59,"fill":90,"stroke":91,"style":62},"104","#ebebfd","var(--brand,#5b5cf0)",[47,93,96],{"x":65,"y":94,"style":95},"128","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca)","workbook structure protection",[47,98,100],{"x":65,"y":99,"style":72},"148","no adding, deleting, renaming or reordering sheets",[40,102],{"x":76,"y":103,"width":78,"height":79,"rx":80,"fill":45,"stroke":91},"116",[47,105,108],{"x":83,"y":106,"style":107},"137","font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","guard rail",[40,110],{"x":55,"y":111,"width":57,"height":58,"rx":59,"fill":112,"stroke":113,"style":62},"170","#fdefd8","var(--gold,#b4740a)",[47,115,118],{"x":65,"y":116,"style":117},"194","font-size:13px;font-weight:700;fill:var(--gold-ink,#7a4e06)","sheet protection + cell locking",[47,120,122],{"x":65,"y":121,"style":72},"214","only the cells you unlock can be typed into",[40,124],{"x":76,"y":125,"width":78,"height":79,"rx":80,"fill":45,"stroke":113},"182",[47,127,108],{"x":83,"y":128,"style":129},"203","font-size:11px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",[40,131],{"x":55,"y":132,"width":57,"height":133,"rx":59,"fill":134,"stroke":135,"style":62},"236","52","#fee8f2","var(--accent,#f43f8f)",[47,137,140],{"x":65,"y":138,"style":139},"258","font-size:13px;font-weight:700;fill:var(--accent-ink,#be185d)","hidden and very hidden sheets",[47,142,144],{"x":65,"y":143,"style":72},"278","tidiness only — the values are plainly readable by any library",[40,146],{"x":76,"y":147,"width":78,"height":79,"rx":80,"fill":45,"stroke":135},"246",[47,149,152],{"x":83,"y":150,"style":151},"267","font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","obscurity",[154,155,157],"h2",{"id":156},"be-honest-about-what-protection-is-for","Be honest about what protection is for",[10,159,160],{},"This is the point to settle before writing any code, because the two things people mean by \"protect this file\" have completely different answers.",[10,162,163,167],{},[164,165,166],"strong",{},"Sheet and workbook protection are interface guards."," They tell Excel which operations to refuse. The password is stored in the file as a short, weak hash that was never designed to resist attack, and openpyxl will happily hand you a protected workbook with every value readable. Their real job is preventing the accident: the reader who tabs into a formula column and overwrites it, or drags a sheet to a new position and breaks a reference.",[10,169,170,173],{},[164,171,172],{},"File encryption is the real control."," The workbook is wrapped in an encrypted container; without the password there is nothing to read. This is what you use when the contents genuinely should not be seen.",[10,175,176],{},"Choosing the wrong one is a common and consequential mistake. A salary report shipped with sheet protection and no encryption is readable by anyone who opens it — the protection stops them typing, not looking.",[10,178,179],{},"The confusion is understandable, because Excel's own vocabulary blurs the line. The menu offers \"Protect Sheet\", \"Protect Workbook\" and \"Encrypt with Password\" in the same place, all under a heading about protection, and only the last of those does anything a security review would recognise. A useful test when deciding which you need: ask what would happen if the file were forwarded to somebody it was not meant for. If the answer is \"they would see everything but could not edit it\", you have chosen a guard rail. If the answer is \"they would see nothing\", you have chosen encryption. Both are legitimate — they simply solve different problems, and a report often wants one layer of each.",[181,182,183,196],"table",{},[184,185,186],"thead",{},[187,188,189,193],"tr",{},[190,191,192],"th",{},"Requirement",[190,194,195],{},"Use",[197,198,199,208,216,224,232,240],"tbody",{},[187,200,201,205],{},[202,203,204],"td",{},"Stop accidental edits to formulas",[202,206,207],{},"Cell locking + sheet protection",[187,209,210,213],{},[202,211,212],{},"Stop sheets being reordered or deleted",[202,214,215],{},"Workbook structure protection",[187,217,218,221],{},[202,219,220],{},"Keep a working sheet out of the way",[202,222,223],{},"Hidden or very hidden sheet",[187,225,226,229],{},[202,227,228],{},"Stop anyone reading the contents",[202,230,231],{},"File encryption",[187,233,234,237],{},[202,235,236],{},"Explain a figure to the reader",[202,238,239],{},"Cell comments",[187,241,242,245],{},[202,243,244],{},"Record who produced the file and when",[202,246,247],{},"Document properties",[154,249,251],{"id":250},"locking-cells-and-protecting-a-sheet","Locking cells and protecting a sheet",[10,253,254,255,258],{},"The mechanic surprises everyone the first time: ",[164,256,257],{},"every cell is locked by default",", and that lock does nothing until sheet protection is switched on. So the pattern is not \"lock the formulas\" — it is \"unlock the inputs, then protect the sheet\".",[20,260,30,266,30,269,30,272,30,274,30,282,30,288,30,293,30,297,30,302,30,305,30,310,30,316,30,321,30,325,30,328,30,332,30,335,30,339,30,343,30,346,30,350,30,354,30,357,30,361,30,365,30,368,30,371,30,375,30,378],{"viewBox":261,"role":23,"ariaLabel":262,"ariaLabelledBy":263,"xmlns":28,"style":29},"0 0 800 246","Two-step locking model: all cells start locked but the lock is inert, so you unlock the input range and then enable sheet protection, after which only the unlocked cells accept typing.",[264,265],"lock-t","lock-d",[32,267,268],{"id":264},"Why you unlock cells rather than lock them",[36,270,271],{"id":265},"Three states of the same sheet. Before protection, every cell carries a locked attribute but the attribute has no effect, so everything is editable. In the middle step the input range has its locked attribute cleared. After sheet protection is enabled, the still-locked cells refuse edits while the cleared input range remains editable. Enabling protection without clearing anything produces a sheet nobody can type into at all.",[40,273],{"x":42,"y":42,"width":43,"height":147,"fill":45},[40,275],{"x":276,"y":277,"width":278,"height":279,"rx":276,"fill":280,"stroke":281,"style":62},"14","44","238","150","#f0f2f5","var(--line,#cdd5e6)",[47,283,287],{"x":284,"y":285,"style":286},"133","34","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","1 · as written",[47,289,292],{"x":284,"y":290,"style":291},"76","font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","every cell locked=True",[47,294,296],{"x":284,"y":89,"style":295},"font-size:11px;fill:var(--text,#172033);text-anchor:middle","but protection is off",[47,298,301],{"x":284,"y":299,"style":300},"132","font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","so the flag does nothing",[40,303],{"x":304,"y":279,"width":125,"height":79,"rx":80,"fill":45,"stroke":281},"42",[47,306,309],{"x":284,"y":307,"style":308},"171","font-size:11px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","everything editable",[311,312],"line",{"x1":313,"y1":314,"x2":315,"y2":314,"stroke":91,"style":62},"252","119","276",[317,318],"polygon",{"points":319,"fill":320},"284,119 272,113 272,125","#5b5cf0",[40,322],{"x":323,"y":277,"width":324,"height":279,"rx":276,"fill":90,"stroke":91,"style":62},"292","216",[47,326,327],{"x":49,"y":285,"style":286},"2 · unlock the inputs",[47,329,331],{"x":49,"y":290,"style":330},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","B2:B10 locked=False",[47,333,334],{"x":49,"y":89,"style":295},"the cells a reader",[47,336,338],{"x":49,"y":337,"style":295},"126","is meant to fill in",[40,340],{"x":341,"y":279,"width":342,"height":79,"rx":80,"fill":45,"stroke":91},"316","168",[47,344,345],{"x":49,"y":307,"style":107},"Protection(locked=False)",[311,347],{"x1":348,"y1":314,"x2":349,"y2":314,"stroke":61,"style":62},"508","532",[317,351],{"points":352,"fill":353},"540,119 528,113 528,125","#0f9488",[40,355],{"x":356,"y":277,"width":278,"height":279,"rx":276,"fill":60,"stroke":61,"style":62},"548",[47,358,360],{"x":359,"y":285,"style":286},"667","3 · protect the sheet",[47,362,364],{"x":359,"y":290,"style":363},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","ws.protection.enable()",[47,366,367],{"x":359,"y":89,"style":295},"locked cells refuse edits",[47,369,370],{"x":359,"y":337,"style":295},"B2:B10 still accepts typing",[40,372],{"x":373,"y":279,"width":374,"height":79,"rx":80,"fill":45,"stroke":61},"572","190",[47,376,377],{"x":359,"y":307,"style":85},"exactly the intended shape",[47,379,381],{"x":49,"y":380,"style":300},"226","skip step 2 and you ship a sheet nobody can type into at all",[383,384,389],"pre",{"className":385,"code":386,"language":387,"meta":388,"style":388},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import Workbook\nfrom openpyxl.styles import Protection, Font, PatternFill\n\nwb = Workbook()\nws = wb.active\nws.title = \"Forecast\"\n\nws[\"A1\"] = \"Region\"\nws[\"B1\"] = \"Units (enter)\"\nws[\"C1\"] = \"Unit price\"\nws[\"D1\"] = \"Revenue\"\nfor cell in ws[1]:\n    cell.font = Font(bold=True)\n\nfor i, (region, price) in enumerate(\n    [(\"North\", 12.5), (\"South\", 11.0), (\"West\", 13.25)], start=2\n):\n    ws[f\"A{i}\"] = region\n    ws[f\"B{i}\"] = 0\n    ws[f\"C{i}\"] = price\n    ws[f\"D{i}\"] = f\"=B{i}*C{i}\"\n\n# Unlock ONLY the input column, and tint it so the reader can see where to type.\nentry = 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\n\nws.protection.sheet = True\nws.protection.password = \"report\"      # deters, does not secure\nws.protection.enable()\n\nwb.save(\"forecast.xlsx\")\n","python","",[390,391,392,410,423,430,442,453,465,470,487,502,517,532,554,577,582,598,645,651,683,708,733,779,784,791,817,868,881,902,913,918,929,943,949,954],"code",{"__ignoreMap":388},[393,394,396,400,404,407],"span",{"class":311,"line":395},1,[393,397,399],{"class":398},"s-kum","from",[393,401,403],{"class":402},"skGVy"," openpyxl ",[393,405,406],{"class":398},"import",[393,408,409],{"class":402}," Workbook\n",[393,411,413,415,418,420],{"class":311,"line":412},2,[393,414,399],{"class":398},[393,416,417],{"class":402}," openpyxl.styles ",[393,419,406],{"class":398},[393,421,422],{"class":402}," Protection, Font, PatternFill\n",[393,424,426],{"class":311,"line":425},3,[393,427,429],{"emptyLinePlaceholder":428},true,"\n",[393,431,433,436,439],{"class":311,"line":432},4,[393,434,435],{"class":402},"wb ",[393,437,438],{"class":398},"=",[393,440,441],{"class":402}," Workbook()\n",[393,443,445,448,450],{"class":311,"line":444},5,[393,446,447],{"class":402},"ws ",[393,449,438],{"class":398},[393,451,452],{"class":402}," wb.active\n",[393,454,456,459,461],{"class":311,"line":455},6,[393,457,458],{"class":402},"ws.title ",[393,460,438],{"class":398},[393,462,464],{"class":463},"srMev"," \"Forecast\"\n",[393,466,468],{"class":311,"line":467},7,[393,469,429],{"emptyLinePlaceholder":428},[393,471,473,476,479,482,484],{"class":311,"line":472},8,[393,474,475],{"class":402},"ws[",[393,477,478],{"class":463},"\"A1\"",[393,480,481],{"class":402},"] ",[393,483,438],{"class":398},[393,485,486],{"class":463}," \"Region\"\n",[393,488,490,492,495,497,499],{"class":311,"line":489},9,[393,491,475],{"class":402},[393,493,494],{"class":463},"\"B1\"",[393,496,481],{"class":402},[393,498,438],{"class":398},[393,500,501],{"class":463}," \"Units (enter)\"\n",[393,503,505,507,510,512,514],{"class":311,"line":504},10,[393,506,475],{"class":402},[393,508,509],{"class":463},"\"C1\"",[393,511,481],{"class":402},[393,513,438],{"class":398},[393,515,516],{"class":463}," \"Unit price\"\n",[393,518,520,522,525,527,529],{"class":311,"line":519},11,[393,521,475],{"class":402},[393,523,524],{"class":463},"\"D1\"",[393,526,481],{"class":402},[393,528,438],{"class":398},[393,530,531],{"class":463}," \"Revenue\"\n",[393,533,535,538,541,544,547,551],{"class":311,"line":534},12,[393,536,537],{"class":398},"for",[393,539,540],{"class":402}," cell ",[393,542,543],{"class":398},"in",[393,545,546],{"class":402}," ws[",[393,548,550],{"class":549},"sP0c6","1",[393,552,553],{"class":402},"]:\n",[393,555,557,560,562,565,569,571,574],{"class":311,"line":556},13,[393,558,559],{"class":402},"    cell.font ",[393,561,438],{"class":398},[393,563,564],{"class":402}," Font(",[393,566,568],{"class":567},"sa561","bold",[393,570,438],{"class":398},[393,572,573],{"class":549},"True",[393,575,576],{"class":402},")\n",[393,578,580],{"class":311,"line":579},14,[393,581,429],{"emptyLinePlaceholder":428},[393,583,585,587,590,592,595],{"class":311,"line":584},15,[393,586,537],{"class":398},[393,588,589],{"class":402}," i, (region, price) ",[393,591,543],{"class":398},[393,593,594],{"class":549}," enumerate",[393,596,597],{"class":402},"(\n",[393,599,601,604,607,610,613,616,619,621,624,626,629,631,634,637,640,642],{"class":311,"line":600},16,[393,602,603],{"class":402},"    [(",[393,605,606],{"class":463},"\"North\"",[393,608,609],{"class":402},", ",[393,611,612],{"class":549},"12.5",[393,614,615],{"class":402},"), (",[393,617,618],{"class":463},"\"South\"",[393,620,609],{"class":402},[393,622,623],{"class":549},"11.0",[393,625,615],{"class":402},[393,627,628],{"class":463},"\"West\"",[393,630,609],{"class":402},[393,632,633],{"class":549},"13.25",[393,635,636],{"class":402},")], ",[393,638,639],{"class":567},"start",[393,641,438],{"class":398},[393,643,644],{"class":549},"2\n",[393,646,648],{"class":311,"line":647},17,[393,649,650],{"class":402},"):\n",[393,652,654,657,660,663,667,670,673,676,678,680],{"class":311,"line":653},18,[393,655,656],{"class":402},"    ws[",[393,658,659],{"class":398},"f",[393,661,662],{"class":463},"\"A",[393,664,666],{"class":665},"sSjpA","{",[393,668,669],{"class":402},"i",[393,671,672],{"class":665},"}",[393,674,675],{"class":463},"\"",[393,677,481],{"class":402},[393,679,438],{"class":398},[393,681,682],{"class":402}," region\n",[393,684,686,688,690,693,695,697,699,701,703,705],{"class":311,"line":685},19,[393,687,656],{"class":402},[393,689,659],{"class":398},[393,691,692],{"class":463},"\"B",[393,694,666],{"class":665},[393,696,669],{"class":402},[393,698,672],{"class":665},[393,700,675],{"class":463},[393,702,481],{"class":402},[393,704,438],{"class":398},[393,706,707],{"class":549}," 0\n",[393,709,711,713,715,718,720,722,724,726,728,730],{"class":311,"line":710},20,[393,712,656],{"class":402},[393,714,659],{"class":398},[393,716,717],{"class":463},"\"C",[393,719,666],{"class":665},[393,721,669],{"class":402},[393,723,672],{"class":665},[393,725,675],{"class":463},[393,727,481],{"class":402},[393,729,438],{"class":398},[393,731,732],{"class":402}," price\n",[393,734,736,738,740,743,745,747,749,751,753,755,758,761,763,765,767,770,772,774,776],{"class":311,"line":735},21,[393,737,656],{"class":402},[393,739,659],{"class":398},[393,741,742],{"class":463},"\"D",[393,744,666],{"class":665},[393,746,669],{"class":402},[393,748,672],{"class":665},[393,750,675],{"class":463},[393,752,481],{"class":402},[393,754,438],{"class":398},[393,756,757],{"class":398}," f",[393,759,760],{"class":463},"\"=B",[393,762,666],{"class":665},[393,764,669],{"class":402},[393,766,672],{"class":665},[393,768,769],{"class":463},"*C",[393,771,666],{"class":665},[393,773,669],{"class":402},[393,775,672],{"class":665},[393,777,778],{"class":463},"\"\n",[393,780,782],{"class":311,"line":781},22,[393,783,429],{"emptyLinePlaceholder":428},[393,785,787],{"class":311,"line":786},23,[393,788,790],{"class":789},"s-wDw","# Unlock ONLY the input column, and tint it so the reader can see where to type.\n",[393,792,794,797,799,802,805,807,810,812,815],{"class":311,"line":793},24,[393,795,796],{"class":402},"entry ",[393,798,438],{"class":398},[393,800,801],{"class":402}," PatternFill(",[393,803,804],{"class":463},"\"solid\"",[393,806,609],{"class":402},[393,808,809],{"class":567},"fgColor",[393,811,438],{"class":398},[393,813,814],{"class":463},"\"FFF7E6\"",[393,816,576],{"class":402},[393,818,820,822,825,827,830,833,835,838,840,843,845,848,850,853,855,857,859,862,864,866],{"class":311,"line":819},25,[393,821,537],{"class":398},[393,823,824],{"class":402}," row ",[393,826,543],{"class":398},[393,828,829],{"class":402}," ws.iter_rows(",[393,831,832],{"class":567},"min_row",[393,834,438],{"class":398},[393,836,837],{"class":549},"2",[393,839,609],{"class":402},[393,841,842],{"class":567},"max_row",[393,844,438],{"class":398},[393,846,847],{"class":549},"4",[393,849,609],{"class":402},[393,851,852],{"class":567},"min_col",[393,854,438],{"class":398},[393,856,837],{"class":549},[393,858,609],{"class":402},[393,860,861],{"class":567},"max_col",[393,863,438],{"class":398},[393,865,837],{"class":549},[393,867,650],{"class":402},[393,869,871,874,876,878],{"class":311,"line":870},26,[393,872,873],{"class":398},"    for",[393,875,540],{"class":402},[393,877,543],{"class":398},[393,879,880],{"class":402}," row:\n",[393,882,884,887,889,892,895,897,900],{"class":311,"line":883},27,[393,885,886],{"class":402},"        cell.protection ",[393,888,438],{"class":398},[393,890,891],{"class":402}," Protection(",[393,893,894],{"class":567},"locked",[393,896,438],{"class":398},[393,898,899],{"class":549},"False",[393,901,576],{"class":402},[393,903,905,908,910],{"class":311,"line":904},28,[393,906,907],{"class":402},"        cell.fill ",[393,909,438],{"class":398},[393,911,912],{"class":402}," entry\n",[393,914,916],{"class":311,"line":915},29,[393,917,429],{"emptyLinePlaceholder":428},[393,919,921,924,926],{"class":311,"line":920},30,[393,922,923],{"class":402},"ws.protection.sheet ",[393,925,438],{"class":398},[393,927,928],{"class":549}," True\n",[393,930,932,935,937,940],{"class":311,"line":931},31,[393,933,934],{"class":402},"ws.protection.password ",[393,936,438],{"class":398},[393,938,939],{"class":463}," \"report\"",[393,941,942],{"class":789},"      # deters, does not secure\n",[393,944,946],{"class":311,"line":945},32,[393,947,948],{"class":402},"ws.protection.enable()\n",[393,950,952],{"class":311,"line":951},33,[393,953,429],{"emptyLinePlaceholder":428},[393,955,957,960,963],{"class":311,"line":956},34,[393,958,959],{"class":402},"wb.save(",[393,961,962],{"class":463},"\"forecast.xlsx\"",[393,964,576],{"class":402},[10,966,967],{},"Tinting the unlocked range is worth the two extra lines. A protected sheet with no visual cue produces support requests from readers who cannot tell which cells will accept input.",[10,969,970],{},"Sheet protection is not all-or-nothing. Each permission is a separate flag, and the useful default is to keep sorting and filtering available so the sheet stays browsable:",[383,972,974],{"className":385,"code":973,"language":387,"meta":388,"style":388},"ws.protection.sheet = True\nws.protection.autoFilter = False       # False here means \"allowed\"\nws.protection.sort = False\nws.protection.formatCells = True       # True means \"blocked\"\nws.protection.insertRows = True\nws.protection.deleteRows = True\nws.protection.enable()\n",[390,975,976,984,997,1007,1020,1029,1038],{"__ignoreMap":388},[393,977,978,980,982],{"class":311,"line":395},[393,979,923],{"class":402},[393,981,438],{"class":398},[393,983,928],{"class":549},[393,985,986,989,991,994],{"class":311,"line":412},[393,987,988],{"class":402},"ws.protection.autoFilter ",[393,990,438],{"class":398},[393,992,993],{"class":549}," False",[393,995,996],{"class":789},"       # False here means \"allowed\"\n",[393,998,999,1002,1004],{"class":311,"line":425},[393,1000,1001],{"class":402},"ws.protection.sort ",[393,1003,438],{"class":398},[393,1005,1006],{"class":549}," False\n",[393,1008,1009,1012,1014,1017],{"class":311,"line":432},[393,1010,1011],{"class":402},"ws.protection.formatCells ",[393,1013,438],{"class":398},[393,1015,1016],{"class":549}," True",[393,1018,1019],{"class":789},"       # True means \"blocked\"\n",[393,1021,1022,1025,1027],{"class":311,"line":444},[393,1023,1024],{"class":402},"ws.protection.insertRows ",[393,1026,438],{"class":398},[393,1028,928],{"class":549},[393,1030,1031,1034,1036],{"class":311,"line":455},[393,1032,1033],{"class":402},"ws.protection.deleteRows ",[393,1035,438],{"class":398},[393,1037,928],{"class":549},[393,1039,1040],{"class":311,"line":467},[393,1041,948],{"class":402},[10,1043,1044,1045,1049,1050,1052,1053,1055,1056,1059,1060,1063,1064,1066,1067,18],{},"The inverted sense of those flags is a genuine trap: the attribute names what is ",[1046,1047,1048],"em",{},"protected",", so ",[390,1051,899],{}," permits and ",[390,1054,573],{}," blocks. Set ",[390,1057,1058],{},"autoFilter"," and ",[390,1061,1062],{},"sort"," to ",[390,1065,899],{}," on any sheet with a table, or readers lose the filter dropdowns that make it usable — the ones added in ",[14,1068,1070],{"href":1069},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002F","creating Excel tables and autofilters with Python",[10,1072,1073,1074,18],{},"The complete treatment, including protecting only part of a sheet, is in ",[14,1075,1077],{"href":1076},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Flock-cells-and-protect-a-sheet-with-openpyxl\u002F","locking cells and protecting a sheet with openpyxl",[154,1079,1081],{"id":1080},"protecting-the-workbook-structure","Protecting the workbook structure",[10,1083,1084,1085,1088],{},"Sheet protection guards cells. Workbook protection guards the ",[1046,1086,1087],{},"shape"," of the file — which matters when a dashboard's formulas reference sheets by name and a reader renaming one breaks everything:",[383,1090,1092],{"className":385,"code":1091,"language":387,"meta":388,"style":388},"from openpyxl import load_workbook\n\nwb = load_workbook(\"dashboard.xlsx\")\n\nwb.security.lockStructure = True       # no add, delete, rename, reorder, hide\nwb.security.workbookPassword = \"structure\"\n\nwb.save(\"dashboard_locked.xlsx\")\n",[390,1093,1094,1105,1109,1123,1127,1139,1149,1153],{"__ignoreMap":388},[393,1095,1096,1098,1100,1102],{"class":311,"line":395},[393,1097,399],{"class":398},[393,1099,403],{"class":402},[393,1101,406],{"class":398},[393,1103,1104],{"class":402}," load_workbook\n",[393,1106,1107],{"class":311,"line":412},[393,1108,429],{"emptyLinePlaceholder":428},[393,1110,1111,1113,1115,1118,1121],{"class":311,"line":425},[393,1112,435],{"class":402},[393,1114,438],{"class":398},[393,1116,1117],{"class":402}," load_workbook(",[393,1119,1120],{"class":463},"\"dashboard.xlsx\"",[393,1122,576],{"class":402},[393,1124,1125],{"class":311,"line":432},[393,1126,429],{"emptyLinePlaceholder":428},[393,1128,1129,1132,1134,1136],{"class":311,"line":444},[393,1130,1131],{"class":402},"wb.security.lockStructure ",[393,1133,438],{"class":398},[393,1135,1016],{"class":549},[393,1137,1138],{"class":789},"       # no add, delete, rename, reorder, hide\n",[393,1140,1141,1144,1146],{"class":311,"line":455},[393,1142,1143],{"class":402},"wb.security.workbookPassword ",[393,1145,438],{"class":398},[393,1147,1148],{"class":463}," \"structure\"\n",[393,1150,1151],{"class":311,"line":467},[393,1152,429],{"emptyLinePlaceholder":428},[393,1154,1155,1157,1160],{"class":311,"line":472},[393,1156,959],{"class":402},[393,1158,1159],{"class":463},"\"dashboard_locked.xlsx\"",[393,1161,576],{"class":402},[10,1163,1164,1165,1169],{},"This pairs naturally with the multi-sheet reports built in ",[14,1166,1168],{"href":1167},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002F","building multi-sheet Excel dashboards",", where a summary sheet holds cross-sheet references that only work while the sheet names stay put.",[154,1171,1173],{"id":1172},"hidden-and-very-hidden-sheets","Hidden and very hidden sheets",[10,1175,1176],{},"Generated reports often carry sheets the reader does not need: a lookup table feeding data validation, a raw extract behind a pivot, a parameters sheet. Excel has two levels of hiding, and openpyxl exposes both:",[383,1178,1180],{"className":385,"code":1179,"language":387,"meta":388,"style":388},"from openpyxl import load_workbook\n\nwb = load_workbook(\"report.xlsx\")\n\nwb[\"Lookups\"].sheet_state = \"hidden\"        # right-click to unhide\nwb[\"_raw\"].sheet_state = \"veryHidden\"       # needs the VBA editor to reveal\n\nwb.save(\"report_tidy.xlsx\")\n",[390,1181,1182,1192,1196,1209,1213,1232,1249,1253],{"__ignoreMap":388},[393,1183,1184,1186,1188,1190],{"class":311,"line":395},[393,1185,399],{"class":398},[393,1187,403],{"class":402},[393,1189,406],{"class":398},[393,1191,1104],{"class":402},[393,1193,1194],{"class":311,"line":412},[393,1195,429],{"emptyLinePlaceholder":428},[393,1197,1198,1200,1202,1204,1207],{"class":311,"line":425},[393,1199,435],{"class":402},[393,1201,438],{"class":398},[393,1203,1117],{"class":402},[393,1205,1206],{"class":463},"\"report.xlsx\"",[393,1208,576],{"class":402},[393,1210,1211],{"class":311,"line":432},[393,1212,429],{"emptyLinePlaceholder":428},[393,1214,1215,1218,1221,1224,1226,1229],{"class":311,"line":444},[393,1216,1217],{"class":402},"wb[",[393,1219,1220],{"class":463},"\"Lookups\"",[393,1222,1223],{"class":402},"].sheet_state ",[393,1225,438],{"class":398},[393,1227,1228],{"class":463}," \"hidden\"",[393,1230,1231],{"class":789},"        # right-click to unhide\n",[393,1233,1234,1236,1239,1241,1243,1246],{"class":311,"line":455},[393,1235,1217],{"class":402},[393,1237,1238],{"class":463},"\"_raw\"",[393,1240,1223],{"class":402},[393,1242,438],{"class":398},[393,1244,1245],{"class":463}," \"veryHidden\"",[393,1247,1248],{"class":789},"       # needs the VBA editor to reveal\n",[393,1250,1251],{"class":311,"line":467},[393,1252,429],{"emptyLinePlaceholder":428},[393,1254,1255,1257,1260],{"class":311,"line":472},[393,1256,959],{"class":402},[393,1258,1259],{"class":463},"\"report_tidy.xlsx\"",[393,1261,576],{"class":402},[10,1263,1264,1267],{},[390,1265,1266],{},"veryHidden"," keeps a sheet out of the unhide dialog entirely. It is a tidiness control, not a security one — the values are in the file and any library reads them straight through. Use it for the working sheets that would confuse a reader, and never for anything confidential.",[10,1269,1270],{},"One rule Excel enforces: at least one sheet must be visible. Hiding the last visible sheet produces a file Excel reports as corrupt, so a defensive helper is worth having:",[383,1272,1274],{"className":385,"code":1273,"language":387,"meta":388,"style":388},"def hide_sheets(wb, names, state=\"hidden\"):\n    \"\"\"Hide sheets, refusing to hide the last visible one.\"\"\"\n    targets = [n for n in names if n in wb.sheetnames]\n    remaining = [\n        ws.title for ws in wb.worksheets\n        if ws.sheet_state == \"visible\" and ws.title not in targets\n    ]\n    if not remaining:\n        raise ValueError(\"at least one sheet must stay visible\")\n    for name in targets:\n        wb[name].sheet_state = state\n    return targets\n",[390,1275,1276,1295,1300,1330,1340,1355,1384,1389,1400,1416,1428,1438],{"__ignoreMap":388},[393,1277,1278,1281,1285,1288,1290,1293],{"class":311,"line":395},[393,1279,1280],{"class":398},"def",[393,1282,1284],{"class":1283},"s_Opv"," hide_sheets",[393,1286,1287],{"class":402},"(wb, names, state",[393,1289,438],{"class":398},[393,1291,1292],{"class":463},"\"hidden\"",[393,1294,650],{"class":402},[393,1296,1297],{"class":311,"line":412},[393,1298,1299],{"class":463},"    \"\"\"Hide sheets, refusing to hide the last visible one.\"\"\"\n",[393,1301,1302,1305,1307,1310,1312,1315,1317,1320,1323,1325,1327],{"class":311,"line":425},[393,1303,1304],{"class":402},"    targets ",[393,1306,438],{"class":398},[393,1308,1309],{"class":402}," [n ",[393,1311,537],{"class":398},[393,1313,1314],{"class":402}," n ",[393,1316,543],{"class":398},[393,1318,1319],{"class":402}," names ",[393,1321,1322],{"class":398},"if",[393,1324,1314],{"class":402},[393,1326,543],{"class":398},[393,1328,1329],{"class":402}," wb.sheetnames]\n",[393,1331,1332,1335,1337],{"class":311,"line":432},[393,1333,1334],{"class":402},"    remaining ",[393,1336,438],{"class":398},[393,1338,1339],{"class":402}," [\n",[393,1341,1342,1345,1347,1350,1352],{"class":311,"line":444},[393,1343,1344],{"class":402},"        ws.title ",[393,1346,537],{"class":398},[393,1348,1349],{"class":402}," ws ",[393,1351,543],{"class":398},[393,1353,1354],{"class":402}," wb.worksheets\n",[393,1356,1357,1360,1363,1366,1369,1372,1375,1378,1381],{"class":311,"line":455},[393,1358,1359],{"class":398},"        if",[393,1361,1362],{"class":402}," ws.sheet_state ",[393,1364,1365],{"class":398},"==",[393,1367,1368],{"class":463}," \"visible\"",[393,1370,1371],{"class":398}," and",[393,1373,1374],{"class":402}," ws.title ",[393,1376,1377],{"class":398},"not",[393,1379,1380],{"class":398}," in",[393,1382,1383],{"class":402}," targets\n",[393,1385,1386],{"class":311,"line":467},[393,1387,1388],{"class":402},"    ]\n",[393,1390,1391,1394,1397],{"class":311,"line":472},[393,1392,1393],{"class":398},"    if",[393,1395,1396],{"class":398}," not",[393,1398,1399],{"class":402}," remaining:\n",[393,1401,1402,1405,1408,1411,1414],{"class":311,"line":489},[393,1403,1404],{"class":398},"        raise",[393,1406,1407],{"class":549}," ValueError",[393,1409,1410],{"class":402},"(",[393,1412,1413],{"class":463},"\"at least one sheet must stay visible\"",[393,1415,576],{"class":402},[393,1417,1418,1420,1423,1425],{"class":311,"line":504},[393,1419,873],{"class":398},[393,1421,1422],{"class":402}," name ",[393,1424,543],{"class":398},[393,1426,1427],{"class":402}," targets:\n",[393,1429,1430,1433,1435],{"class":311,"line":519},[393,1431,1432],{"class":402},"        wb[name].sheet_state ",[393,1434,438],{"class":398},[393,1436,1437],{"class":402}," state\n",[393,1439,1440,1443],{"class":311,"line":534},[393,1441,1442],{"class":398},"    return",[393,1444,1383],{"class":402},[10,1446,1447,1448,18],{},"Hiding individual rows and columns follows the same idea at a finer grain, covered in ",[14,1449,1451],{"href":1450},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fhide-sheets-rows-and-columns-with-openpyxl\u002F","hiding sheets, rows and columns with openpyxl",[154,1453,1455],{"id":1454},"encrypting-the-file","Encrypting the file",[10,1457,1458],{},"When the contents genuinely must not be read, none of the above helps. You need the encrypted container, and that means a separate package — neither openpyxl nor xlsxwriter can write one.",[383,1460,1464],{"className":1461,"code":1462,"language":1463,"meta":388,"style":388},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install msoffcrypto-tool\n","bash",[390,1465,1466],{"__ignoreMap":388},[393,1467,1468,1472,1475],{"class":311,"line":395},[393,1469,1471],{"class":1470},"sMTad","pip",[393,1473,1474],{"class":463}," install",[393,1476,1477],{"class":463}," msoffcrypto-tool\n",[383,1479,1481],{"className":385,"code":1480,"language":387,"meta":388,"style":388},"import io\nimport os\nimport msoffcrypto\nimport pandas as pd\n\ndef write_encrypted(df, path, password):\n    \"\"\"Write a DataFrame to a real password-encrypted .xlsx.\"\"\"\n    plain = io.BytesIO()\n    with pd.ExcelWriter(plain, engine=\"xlsxwriter\") as writer:\n        df.to_excel(writer, sheet_name=\"Summary\", index=False)\n    plain.seek(0)\n\n    office = msoffcrypto.OfficeFile(plain)\n    with open(path, \"wb\") as out:\n        office.encrypt(password, out)\n    return path\n\ndf = pd.DataFrame({\"employee\": [\"A. Chen\", \"B. Ortiz\"], \"salary\": [72000, 68500]})\nwrite_encrypted(df, \"payroll.xlsx\", os.environ[\"REPORT_PASSWORD\"])\n",[390,1482,1483,1490,1497,1504,1517,1521,1531,1536,1546,1570,1594,1603,1607,1617,1637,1642,1649,1653,1696],{"__ignoreMap":388},[393,1484,1485,1487],{"class":311,"line":395},[393,1486,406],{"class":398},[393,1488,1489],{"class":402}," io\n",[393,1491,1492,1494],{"class":311,"line":412},[393,1493,406],{"class":398},[393,1495,1496],{"class":402}," os\n",[393,1498,1499,1501],{"class":311,"line":425},[393,1500,406],{"class":398},[393,1502,1503],{"class":402}," msoffcrypto\n",[393,1505,1506,1508,1511,1514],{"class":311,"line":432},[393,1507,406],{"class":398},[393,1509,1510],{"class":402}," pandas ",[393,1512,1513],{"class":398},"as",[393,1515,1516],{"class":402}," pd\n",[393,1518,1519],{"class":311,"line":444},[393,1520,429],{"emptyLinePlaceholder":428},[393,1522,1523,1525,1528],{"class":311,"line":455},[393,1524,1280],{"class":398},[393,1526,1527],{"class":1283}," write_encrypted",[393,1529,1530],{"class":402},"(df, path, password):\n",[393,1532,1533],{"class":311,"line":467},[393,1534,1535],{"class":463},"    \"\"\"Write a DataFrame to a real password-encrypted .xlsx.\"\"\"\n",[393,1537,1538,1541,1543],{"class":311,"line":472},[393,1539,1540],{"class":402},"    plain ",[393,1542,438],{"class":398},[393,1544,1545],{"class":402}," io.BytesIO()\n",[393,1547,1548,1551,1554,1557,1559,1562,1565,1567],{"class":311,"line":489},[393,1549,1550],{"class":398},"    with",[393,1552,1553],{"class":402}," pd.ExcelWriter(plain, ",[393,1555,1556],{"class":567},"engine",[393,1558,438],{"class":398},[393,1560,1561],{"class":463},"\"xlsxwriter\"",[393,1563,1564],{"class":402},") ",[393,1566,1513],{"class":398},[393,1568,1569],{"class":402}," writer:\n",[393,1571,1572,1575,1578,1580,1583,1585,1588,1590,1592],{"class":311,"line":504},[393,1573,1574],{"class":402},"        df.to_excel(writer, ",[393,1576,1577],{"class":567},"sheet_name",[393,1579,438],{"class":398},[393,1581,1582],{"class":463},"\"Summary\"",[393,1584,609],{"class":402},[393,1586,1587],{"class":567},"index",[393,1589,438],{"class":398},[393,1591,899],{"class":549},[393,1593,576],{"class":402},[393,1595,1596,1599,1601],{"class":311,"line":519},[393,1597,1598],{"class":402},"    plain.seek(",[393,1600,42],{"class":549},[393,1602,576],{"class":402},[393,1604,1605],{"class":311,"line":534},[393,1606,429],{"emptyLinePlaceholder":428},[393,1608,1609,1612,1614],{"class":311,"line":556},[393,1610,1611],{"class":402},"    office ",[393,1613,438],{"class":398},[393,1615,1616],{"class":402}," msoffcrypto.OfficeFile(plain)\n",[393,1618,1619,1621,1624,1627,1630,1632,1634],{"class":311,"line":579},[393,1620,1550],{"class":398},[393,1622,1623],{"class":549}," open",[393,1625,1626],{"class":402},"(path, ",[393,1628,1629],{"class":463},"\"wb\"",[393,1631,1564],{"class":402},[393,1633,1513],{"class":398},[393,1635,1636],{"class":402}," out:\n",[393,1638,1639],{"class":311,"line":584},[393,1640,1641],{"class":402},"        office.encrypt(password, out)\n",[393,1643,1644,1646],{"class":311,"line":600},[393,1645,1442],{"class":398},[393,1647,1648],{"class":402}," path\n",[393,1650,1651],{"class":311,"line":647},[393,1652,429],{"emptyLinePlaceholder":428},[393,1654,1655,1658,1660,1663,1666,1669,1672,1674,1677,1680,1683,1685,1688,1690,1693],{"class":311,"line":653},[393,1656,1657],{"class":402},"df ",[393,1659,438],{"class":398},[393,1661,1662],{"class":402}," pd.DataFrame({",[393,1664,1665],{"class":463},"\"employee\"",[393,1667,1668],{"class":402},": [",[393,1670,1671],{"class":463},"\"A. Chen\"",[393,1673,609],{"class":402},[393,1675,1676],{"class":463},"\"B. Ortiz\"",[393,1678,1679],{"class":402},"], ",[393,1681,1682],{"class":463},"\"salary\"",[393,1684,1668],{"class":402},[393,1686,1687],{"class":549},"72000",[393,1689,609],{"class":402},[393,1691,1692],{"class":549},"68500",[393,1694,1695],{"class":402},"]})\n",[393,1697,1698,1701,1704,1707,1710],{"class":311,"line":685},[393,1699,1700],{"class":402},"write_encrypted(df, ",[393,1702,1703],{"class":463},"\"payroll.xlsx\"",[393,1705,1706],{"class":402},", os.environ[",[393,1708,1709],{"class":463},"\"REPORT_PASSWORD\"",[393,1711,1712],{"class":402},"])\n",[10,1714,1715],{},"Reading one back needs the same package, and this is the piece worth wiring into an ingest pipeline that receives encrypted files from a bank or a payroll provider:",[383,1717,1719],{"className":385,"code":1718,"language":387,"meta":388,"style":388},"import io\nimport msoffcrypto\nimport pandas as pd\n\ndef read_encrypted(path, password):\n    decrypted = io.BytesIO()\n    with open(path, \"rb\") as fh:\n        office = msoffcrypto.OfficeFile(fh)\n        office.load_key(password=password)\n        office.decrypt(decrypted)\n    decrypted.seek(0)\n    return pd.read_excel(decrypted)\n\ndf = read_encrypted(\"payroll.xlsx\", os.environ[\"REPORT_PASSWORD\"])\n",[390,1720,1721,1727,1733,1743,1747,1757,1766,1784,1794,1807,1812,1821,1828,1832],{"__ignoreMap":388},[393,1722,1723,1725],{"class":311,"line":395},[393,1724,406],{"class":398},[393,1726,1489],{"class":402},[393,1728,1729,1731],{"class":311,"line":412},[393,1730,406],{"class":398},[393,1732,1503],{"class":402},[393,1734,1735,1737,1739,1741],{"class":311,"line":425},[393,1736,406],{"class":398},[393,1738,1510],{"class":402},[393,1740,1513],{"class":398},[393,1742,1516],{"class":402},[393,1744,1745],{"class":311,"line":432},[393,1746,429],{"emptyLinePlaceholder":428},[393,1748,1749,1751,1754],{"class":311,"line":444},[393,1750,1280],{"class":398},[393,1752,1753],{"class":1283}," read_encrypted",[393,1755,1756],{"class":402},"(path, password):\n",[393,1758,1759,1762,1764],{"class":311,"line":455},[393,1760,1761],{"class":402},"    decrypted ",[393,1763,438],{"class":398},[393,1765,1545],{"class":402},[393,1767,1768,1770,1772,1774,1777,1779,1781],{"class":311,"line":467},[393,1769,1550],{"class":398},[393,1771,1623],{"class":549},[393,1773,1626],{"class":402},[393,1775,1776],{"class":463},"\"rb\"",[393,1778,1564],{"class":402},[393,1780,1513],{"class":398},[393,1782,1783],{"class":402}," fh:\n",[393,1785,1786,1789,1791],{"class":311,"line":472},[393,1787,1788],{"class":402},"        office ",[393,1790,438],{"class":398},[393,1792,1793],{"class":402}," msoffcrypto.OfficeFile(fh)\n",[393,1795,1796,1799,1802,1804],{"class":311,"line":489},[393,1797,1798],{"class":402},"        office.load_key(",[393,1800,1801],{"class":567},"password",[393,1803,438],{"class":398},[393,1805,1806],{"class":402},"password)\n",[393,1808,1809],{"class":311,"line":504},[393,1810,1811],{"class":402},"        office.decrypt(decrypted)\n",[393,1813,1814,1817,1819],{"class":311,"line":519},[393,1815,1816],{"class":402},"    decrypted.seek(",[393,1818,42],{"class":549},[393,1820,576],{"class":402},[393,1822,1823,1825],{"class":311,"line":534},[393,1824,1442],{"class":398},[393,1826,1827],{"class":402}," pd.read_excel(decrypted)\n",[393,1829,1830],{"class":311,"line":556},[393,1831,429],{"emptyLinePlaceholder":428},[393,1833,1834,1836,1838,1841,1843,1845,1847],{"class":311,"line":579},[393,1835,1657],{"class":402},[393,1837,438],{"class":398},[393,1839,1840],{"class":402}," read_encrypted(",[393,1842,1703],{"class":463},[393,1844,1706],{"class":402},[393,1846,1709],{"class":463},[393,1848,1712],{"class":402},[10,1850,1851,1852,18],{},"Two operational points. Read the password from the environment or a secrets manager — never from source, where it is visible to everyone with repository access. And decide deliberately how the password reaches the recipient: emailing an encrypted workbook and its password in the same message provides no protection at all, which is a surprisingly common pattern. The delivery side is covered in ",[14,1853,1855],{"href":1854},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002F","emailing Excel reports with smtplib",[154,1857,1859],{"id":1858},"comments-and-telling-readers-why","Comments, and telling readers why",[10,1861,1862],{},"Protection stops the wrong edit. A comment explains why the cell is what it is, which prevents the argument that follows. openpyxl attaches them directly:",[383,1864,1866],{"className":385,"code":1865,"language":387,"meta":388,"style":388},"from openpyxl import load_workbook\nfrom openpyxl.comments import Comment\n\nwb = load_workbook(\"forecast.xlsx\")\nws = wb[\"Forecast\"]\n\nnote = Comment(\n    \"Unit price from the Q3 price list, effective 1 July.\\n\"\n    \"Update the Lookups sheet, not this cell.\",\n    \"Reporting\",\n)\nnote.width, note.height = 260, 90\nws[\"C2\"].comment = note\n\nwb.save(\"forecast_annotated.xlsx\")\n",[390,1867,1868,1878,1890,1894,1906,1921,1925,1935,1945,1953,1960,1964,1979,1994,1998],{"__ignoreMap":388},[393,1869,1870,1872,1874,1876],{"class":311,"line":395},[393,1871,399],{"class":398},[393,1873,403],{"class":402},[393,1875,406],{"class":398},[393,1877,1104],{"class":402},[393,1879,1880,1882,1885,1887],{"class":311,"line":412},[393,1881,399],{"class":398},[393,1883,1884],{"class":402}," openpyxl.comments ",[393,1886,406],{"class":398},[393,1888,1889],{"class":402}," Comment\n",[393,1891,1892],{"class":311,"line":425},[393,1893,429],{"emptyLinePlaceholder":428},[393,1895,1896,1898,1900,1902,1904],{"class":311,"line":432},[393,1897,435],{"class":402},[393,1899,438],{"class":398},[393,1901,1117],{"class":402},[393,1903,962],{"class":463},[393,1905,576],{"class":402},[393,1907,1908,1910,1912,1915,1918],{"class":311,"line":444},[393,1909,447],{"class":402},[393,1911,438],{"class":398},[393,1913,1914],{"class":402}," wb[",[393,1916,1917],{"class":463},"\"Forecast\"",[393,1919,1920],{"class":402},"]\n",[393,1922,1923],{"class":311,"line":455},[393,1924,429],{"emptyLinePlaceholder":428},[393,1926,1927,1930,1932],{"class":311,"line":467},[393,1928,1929],{"class":402},"note ",[393,1931,438],{"class":398},[393,1933,1934],{"class":402}," Comment(\n",[393,1936,1937,1940,1943],{"class":311,"line":472},[393,1938,1939],{"class":463},"    \"Unit price from the Q3 price list, effective 1 July.",[393,1941,1942],{"class":665},"\\n",[393,1944,778],{"class":463},[393,1946,1947,1950],{"class":311,"line":489},[393,1948,1949],{"class":463},"    \"Update the Lookups sheet, not this cell.\"",[393,1951,1952],{"class":402},",\n",[393,1954,1955,1958],{"class":311,"line":504},[393,1956,1957],{"class":463},"    \"Reporting\"",[393,1959,1952],{"class":402},[393,1961,1962],{"class":311,"line":519},[393,1963,576],{"class":402},[393,1965,1966,1969,1971,1974,1976],{"class":311,"line":534},[393,1967,1968],{"class":402},"note.width, note.height ",[393,1970,438],{"class":398},[393,1972,1973],{"class":549}," 260",[393,1975,609],{"class":402},[393,1977,1978],{"class":549},"90\n",[393,1980,1981,1983,1986,1989,1991],{"class":311,"line":556},[393,1982,475],{"class":402},[393,1984,1985],{"class":463},"\"C2\"",[393,1987,1988],{"class":402},"].comment ",[393,1990,438],{"class":398},[393,1992,1993],{"class":402}," note\n",[393,1995,1996],{"class":311,"line":579},[393,1997,429],{"emptyLinePlaceholder":428},[393,1999,2000,2002,2005],{"class":311,"line":584},[393,2001,959],{"class":402},[393,2003,2004],{"class":463},"\"forecast_annotated.xlsx\"",[393,2006,576],{"class":402},[10,2008,2009,2010,18],{},"Comments carry an author and appear on hover, which makes them the natural place for the provenance of a number — the source system, the as-at date, the assumption behind a forecast. The full walkthrough is in ",[14,2011,2013],{"href":2012},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fadd-comments-and-notes-to-excel-cells-with-python\u002F","adding comments and notes to Excel cells with Python",[10,2015,2016],{},"Document properties do the same job at file level, and they survive being forwarded in a way that a covering email does not:",[383,2018,2020],{"className":385,"code":2019,"language":387,"meta":388,"style":388},"from datetime import datetime\nfrom openpyxl import load_workbook\n\nwb = load_workbook(\"forecast.xlsx\")\nprops = wb.properties\nprops.title = \"Regional revenue forecast\"\nprops.creator = \"Reporting automation\"\nprops.description = \"Generated nightly from the sales warehouse. Do not edit.\"\nprops.created = datetime(2026, 8, 15, 6, 0)\nprops.modified = datetime(2026, 8, 15, 6, 0)\nwb.save(\"forecast.xlsx\")\n",[390,2021,2022,2034,2044,2048,2060,2070,2080,2090,2100,2133,2162],{"__ignoreMap":388},[393,2023,2024,2026,2029,2031],{"class":311,"line":395},[393,2025,399],{"class":398},[393,2027,2028],{"class":402}," datetime ",[393,2030,406],{"class":398},[393,2032,2033],{"class":402}," datetime\n",[393,2035,2036,2038,2040,2042],{"class":311,"line":412},[393,2037,399],{"class":398},[393,2039,403],{"class":402},[393,2041,406],{"class":398},[393,2043,1104],{"class":402},[393,2045,2046],{"class":311,"line":425},[393,2047,429],{"emptyLinePlaceholder":428},[393,2049,2050,2052,2054,2056,2058],{"class":311,"line":432},[393,2051,435],{"class":402},[393,2053,438],{"class":398},[393,2055,1117],{"class":402},[393,2057,962],{"class":463},[393,2059,576],{"class":402},[393,2061,2062,2065,2067],{"class":311,"line":444},[393,2063,2064],{"class":402},"props ",[393,2066,438],{"class":398},[393,2068,2069],{"class":402}," wb.properties\n",[393,2071,2072,2075,2077],{"class":311,"line":455},[393,2073,2074],{"class":402},"props.title ",[393,2076,438],{"class":398},[393,2078,2079],{"class":463}," \"Regional revenue forecast\"\n",[393,2081,2082,2085,2087],{"class":311,"line":467},[393,2083,2084],{"class":402},"props.creator ",[393,2086,438],{"class":398},[393,2088,2089],{"class":463}," \"Reporting automation\"\n",[393,2091,2092,2095,2097],{"class":311,"line":472},[393,2093,2094],{"class":402},"props.description ",[393,2096,438],{"class":398},[393,2098,2099],{"class":463}," \"Generated nightly from the sales warehouse. Do not edit.\"\n",[393,2101,2102,2105,2107,2110,2113,2115,2117,2119,2122,2124,2127,2129,2131],{"class":311,"line":489},[393,2103,2104],{"class":402},"props.created ",[393,2106,438],{"class":398},[393,2108,2109],{"class":402}," datetime(",[393,2111,2112],{"class":549},"2026",[393,2114,609],{"class":402},[393,2116,80],{"class":549},[393,2118,609],{"class":402},[393,2120,2121],{"class":549},"15",[393,2123,609],{"class":402},[393,2125,2126],{"class":549},"6",[393,2128,609],{"class":402},[393,2130,42],{"class":549},[393,2132,576],{"class":402},[393,2134,2135,2138,2140,2142,2144,2146,2148,2150,2152,2154,2156,2158,2160],{"class":311,"line":504},[393,2136,2137],{"class":402},"props.modified ",[393,2139,438],{"class":398},[393,2141,2109],{"class":402},[393,2143,2112],{"class":549},[393,2145,609],{"class":402},[393,2147,80],{"class":549},[393,2149,609],{"class":402},[393,2151,2121],{"class":549},[393,2153,609],{"class":402},[393,2155,2126],{"class":549},[393,2157,609],{"class":402},[393,2159,42],{"class":549},[393,2161,576],{"class":402},[393,2163,2164,2166,2168],{"class":311,"line":519},[393,2165,959],{"class":402},[393,2167,962],{"class":463},[393,2169,576],{"class":402},[154,2171,2173],{"id":2172},"marking-a-workbook-read-only-and-the-alternative","Marking a workbook read-only, and the alternative",[10,2175,2176],{},"Excel has a softer control than protection: the read-only recommendation. It shows a prompt when the file opens, suggesting the reader open a copy instead. openpyxl exposes it through the same security object:",[383,2178,2180],{"className":385,"code":2179,"language":387,"meta":388,"style":388},"from openpyxl import load_workbook\n\nwb = load_workbook(\"report.xlsx\")\nwb.security.lockStructure = True\nwb.security.revisionsPassword = \"audit\"   # blocks turning off change tracking\nwb.save(\"report_final.xlsx\")\n",[390,2181,2182,2192,2196,2208,2216,2229],{"__ignoreMap":388},[393,2183,2184,2186,2188,2190],{"class":311,"line":395},[393,2185,399],{"class":398},[393,2187,403],{"class":402},[393,2189,406],{"class":398},[393,2191,1104],{"class":402},[393,2193,2194],{"class":311,"line":412},[393,2195,429],{"emptyLinePlaceholder":428},[393,2197,2198,2200,2202,2204,2206],{"class":311,"line":425},[393,2199,435],{"class":402},[393,2201,438],{"class":398},[393,2203,1117],{"class":402},[393,2205,1206],{"class":463},[393,2207,576],{"class":402},[393,2209,2210,2212,2214],{"class":311,"line":432},[393,2211,1131],{"class":402},[393,2213,438],{"class":398},[393,2215,928],{"class":549},[393,2217,2218,2221,2223,2226],{"class":311,"line":444},[393,2219,2220],{"class":402},"wb.security.revisionsPassword ",[393,2222,438],{"class":398},[393,2224,2225],{"class":463}," \"audit\"",[393,2227,2228],{"class":789},"   # blocks turning off change tracking\n",[393,2230,2231,2233,2236],{"class":311,"line":455},[393,2232,959],{"class":402},[393,2234,2235],{"class":463},"\"report_final.xlsx\"",[393,2237,576],{"class":402},[10,2239,2240],{},"Be realistic about its effect: it is a dialog, and readers dismiss dialogs. Where the report genuinely must not be edited, the better answer is usually to stop sending a spreadsheet at all.",[10,2242,2243],{},"That is worth stating plainly, because it solves a whole class of problems at once. A large share of \"Excel reports\" are read and filed, never edited. Send those as PDF and the questions about protection, locking and versioning all disappear — nobody holds a divergent edited copy, because there is nothing to edit:",[383,2245,2247],{"className":385,"code":2246,"language":387,"meta":388,"style":388},"from pathlib import Path\nimport shutil, subprocess, tempfile\n\ndef to_pdf(xlsx_path, out_dir=\"delivery\"):\n    \"\"\"Render a finished workbook to PDF for distribution.\"\"\"\n    soffice = shutil.which(\"soffice\") or shutil.which(\"libreoffice\")\n    if soffice is None:\n        raise RuntimeError(\"LibreOffice not found\")\n    out = Path(out_dir).resolve()\n    out.mkdir(parents=True, exist_ok=True)\n    with tempfile.TemporaryDirectory() as profile:\n        subprocess.run(\n            [soffice, f\"-env:UserInstallation=file:\u002F\u002F{profile}\",\n             \"--headless\", \"--convert-to\", \"pdf\",\n             \"--outdir\", str(out), str(Path(xlsx_path).resolve())],\n            capture_output=True, text=True, timeout=180, check=True,\n        )\n    return out \u002F (Path(xlsx_path).stem + \".pdf\")\n",[390,2248,2249,2261,2268,2272,2289,2294,2319,2335,2349,2359,2382,2394,2399,2420,2437,2455,2493,2498],{"__ignoreMap":388},[393,2250,2251,2253,2256,2258],{"class":311,"line":395},[393,2252,399],{"class":398},[393,2254,2255],{"class":402}," pathlib ",[393,2257,406],{"class":398},[393,2259,2260],{"class":402}," Path\n",[393,2262,2263,2265],{"class":311,"line":412},[393,2264,406],{"class":398},[393,2266,2267],{"class":402}," shutil, subprocess, tempfile\n",[393,2269,2270],{"class":311,"line":425},[393,2271,429],{"emptyLinePlaceholder":428},[393,2273,2274,2276,2279,2282,2284,2287],{"class":311,"line":432},[393,2275,1280],{"class":398},[393,2277,2278],{"class":1283}," to_pdf",[393,2280,2281],{"class":402},"(xlsx_path, out_dir",[393,2283,438],{"class":398},[393,2285,2286],{"class":463},"\"delivery\"",[393,2288,650],{"class":402},[393,2290,2291],{"class":311,"line":444},[393,2292,2293],{"class":463},"    \"\"\"Render a finished workbook to PDF for distribution.\"\"\"\n",[393,2295,2296,2299,2301,2304,2307,2309,2312,2314,2317],{"class":311,"line":455},[393,2297,2298],{"class":402},"    soffice ",[393,2300,438],{"class":398},[393,2302,2303],{"class":402}," shutil.which(",[393,2305,2306],{"class":463},"\"soffice\"",[393,2308,1564],{"class":402},[393,2310,2311],{"class":398},"or",[393,2313,2303],{"class":402},[393,2315,2316],{"class":463},"\"libreoffice\"",[393,2318,576],{"class":402},[393,2320,2321,2323,2326,2329,2332],{"class":311,"line":467},[393,2322,1393],{"class":398},[393,2324,2325],{"class":402}," soffice ",[393,2327,2328],{"class":398},"is",[393,2330,2331],{"class":549}," None",[393,2333,2334],{"class":402},":\n",[393,2336,2337,2339,2342,2344,2347],{"class":311,"line":472},[393,2338,1404],{"class":398},[393,2340,2341],{"class":549}," RuntimeError",[393,2343,1410],{"class":402},[393,2345,2346],{"class":463},"\"LibreOffice not found\"",[393,2348,576],{"class":402},[393,2350,2351,2354,2356],{"class":311,"line":489},[393,2352,2353],{"class":402},"    out ",[393,2355,438],{"class":398},[393,2357,2358],{"class":402}," Path(out_dir).resolve()\n",[393,2360,2361,2364,2367,2369,2371,2373,2376,2378,2380],{"class":311,"line":504},[393,2362,2363],{"class":402},"    out.mkdir(",[393,2365,2366],{"class":567},"parents",[393,2368,438],{"class":398},[393,2370,573],{"class":549},[393,2372,609],{"class":402},[393,2374,2375],{"class":567},"exist_ok",[393,2377,438],{"class":398},[393,2379,573],{"class":549},[393,2381,576],{"class":402},[393,2383,2384,2386,2389,2391],{"class":311,"line":519},[393,2385,1550],{"class":398},[393,2387,2388],{"class":402}," tempfile.TemporaryDirectory() ",[393,2390,1513],{"class":398},[393,2392,2393],{"class":402}," profile:\n",[393,2395,2396],{"class":311,"line":534},[393,2397,2398],{"class":402},"        subprocess.run(\n",[393,2400,2401,2404,2406,2409,2411,2414,2416,2418],{"class":311,"line":556},[393,2402,2403],{"class":402},"            [soffice, ",[393,2405,659],{"class":398},[393,2407,2408],{"class":463},"\"-env:UserInstallation=file:\u002F\u002F",[393,2410,666],{"class":665},[393,2412,2413],{"class":402},"profile",[393,2415,672],{"class":665},[393,2417,675],{"class":463},[393,2419,1952],{"class":402},[393,2421,2422,2425,2427,2430,2432,2435],{"class":311,"line":579},[393,2423,2424],{"class":463},"             \"--headless\"",[393,2426,609],{"class":402},[393,2428,2429],{"class":463},"\"--convert-to\"",[393,2431,609],{"class":402},[393,2433,2434],{"class":463},"\"pdf\"",[393,2436,1952],{"class":402},[393,2438,2439,2442,2444,2447,2450,2452],{"class":311,"line":584},[393,2440,2441],{"class":463},"             \"--outdir\"",[393,2443,609],{"class":402},[393,2445,2446],{"class":549},"str",[393,2448,2449],{"class":402},"(out), ",[393,2451,2446],{"class":549},[393,2453,2454],{"class":402},"(Path(xlsx_path).resolve())],\n",[393,2456,2457,2460,2462,2464,2466,2468,2470,2472,2474,2477,2479,2482,2484,2487,2489,2491],{"class":311,"line":600},[393,2458,2459],{"class":567},"            capture_output",[393,2461,438],{"class":398},[393,2463,573],{"class":549},[393,2465,609],{"class":402},[393,2467,47],{"class":567},[393,2469,438],{"class":398},[393,2471,573],{"class":549},[393,2473,609],{"class":402},[393,2475,2476],{"class":567},"timeout",[393,2478,438],{"class":398},[393,2480,2481],{"class":549},"180",[393,2483,609],{"class":402},[393,2485,2486],{"class":567},"check",[393,2488,438],{"class":398},[393,2490,573],{"class":549},[393,2492,1952],{"class":402},[393,2494,2495],{"class":311,"line":647},[393,2496,2497],{"class":402},"        )\n",[393,2499,2500,2502,2505,2508,2511,2514,2517],{"class":311,"line":653},[393,2501,1442],{"class":398},[393,2503,2504],{"class":402}," out ",[393,2506,2507],{"class":398},"\u002F",[393,2509,2510],{"class":402}," (Path(xlsx_path).stem ",[393,2512,2513],{"class":398},"+",[393,2515,2516],{"class":463}," \".pdf\"",[393,2518,576],{"class":402},[10,2520,2521,2522,2526],{},"The full conversion recipe, including page setup so wide tables paginate sensibly, is in ",[14,2523,2525],{"href":2524},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-file-to-pdf-with-python\u002F","converting an Excel file to PDF with Python",". A common shape for a monthly pack is to send the PDF to everyone and the workbook only to the handful of people who need to model with it.",[154,2528,2530],{"id":2529},"what-survives-and-what-your-script-silently-destroys","What survives, and what your script silently destroys",[10,2532,2533],{},"Protection settings are workbook state, and state is exactly what gets lost when a script rewrites a file rather than editing it. This is the failure people hit after everything above is working: the hardening step runs, the report ships, and the protection is gone — because a later stage wrote the sheet again through pandas.",[20,2535,30,2541,30,2544,30,2547,30,2550,30,2554,30,2560,30,2565,30,2570,30,2582,30,2588,30,2593,30,2596,30,2599,30,2604,30,2608,30,2613,30,2617,30,2620,30,2623,30,2626,30,2631,30,2634,30,2637,30,2641],{"viewBox":2536,"role":23,"ariaLabel":2537,"ariaLabelledBy":2538,"xmlns":28,"style":29},"0 0 800 244","Two pipelines: writing a sheet with pandas to_excel replaces it and discards protection, comments and hidden state, whereas loading with openpyxl and editing cells preserves all of it.",[2539,2540],"survive-t","survive-d",[32,2542,2543],{"id":2539},"Which write path keeps the protection you applied",[36,2545,2546],{"id":2540},"Two paths from a hardened workbook. The upper path calls to_excel on the same sheet name, which replaces the whole sheet and discards protection settings, cell comments, hidden state and column widths. The lower path loads the workbook with openpyxl and assigns to individual cells, leaving every other property intact. The rule that follows is to harden last, after all data has been written.",[40,2548],{"x":42,"y":42,"width":43,"height":2549,"fill":45},"244",[40,2551],{"x":276,"y":65,"width":279,"height":2552,"rx":2553,"fill":60,"stroke":61,"style":62},"70","13",[47,2555,2559],{"x":2556,"y":2557,"style":2558},"89","74","font-size:11px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","hardened file",[47,2561,2564],{"x":2556,"y":2562,"style":2563},"118","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","protection set",[47,2566,2569],{"x":2556,"y":2567,"style":2568},"138","font-size:10.5px;fill:var(--muted,#5b6780);text-anchor:middle","comments, hidden sheets",[2571,2572,2574,2575,2574,2579,30],"g",{"stroke":281,"style":62,"fill":2573},"none","\n    ",[2576,2577],"path",{"d":2578},"M164 112 H 196 V 56 H 226",[2576,2580],{"d":2581},"M164 138 H 196 V 190 H 226",[40,2583],{"x":2584,"y":2585,"width":2586,"height":2587,"rx":59,"fill":134,"stroke":135,"style":62},"234","30","240","54",[47,2589,2592],{"x":2590,"y":133,"style":2591},"354","font-size:11.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","df.to_excel(same sheet)",[47,2594,2595],{"x":2590,"y":84,"style":2568},"the sheet is replaced wholesale",[40,2597],{"x":2584,"y":2598,"width":2586,"height":2587,"rx":59,"fill":90,"stroke":91,"style":62},"164",[47,2600,2603],{"x":2590,"y":2601,"style":2602},"186","font-size:11.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","load_workbook + cell writes",[47,2605,2607],{"x":2590,"y":2606,"style":2568},"205","only the cells you touch change",[311,2609],{"x1":2610,"y1":2611,"x2":2612,"y2":2611,"stroke":135,"style":62},"474","57","510",[317,2614],{"points":2615,"fill":2616},"518,57 506,51 506,63","#f43f8f",[311,2618],{"x1":2610,"y1":2619,"x2":2612,"y2":2619,"stroke":91,"style":62},"191",[317,2621],{"points":2622,"fill":320},"518,191 506,185 506,197",[40,2624],{"x":2625,"y":50,"width":138,"height":66,"rx":59,"fill":134,"stroke":135,"style":62},"526",[47,2627,2630],{"x":2628,"y":77,"style":2629},"655","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","protection lost",[47,2632,2633],{"x":2628,"y":2552,"style":2568},"comments and widths lost too",[40,2635],{"x":2625,"y":2636,"width":138,"height":66,"rx":59,"fill":60,"stroke":61,"style":62},"160",[47,2638,2640],{"x":2628,"y":2639,"style":363},"184","everything preserved",[47,2642,2644],{"x":2628,"y":2643,"style":2568},"204","the safe way to update data",[10,2646,2647,2648,2651],{},"The rule that falls out of this is simple and worth enforcing in the structure of your script: ",[164,2649,2650],{},"harden last",". Generate all the data, write every sheet, then apply protection, hiding and encryption as the final step before delivery. Anything that rewrites a sheet after hardening undoes it.",[10,2653,2654],{},"A quick assertion catches a regression in that ordering before the file goes anywhere:",[383,2656,2658],{"className":385,"code":2657,"language":387,"meta":388,"style":388},"from openpyxl import load_workbook\n\ndef assert_hardened(path, expect_hidden=()):\n    wb = load_workbook(path)\n    for ws in wb.worksheets:\n        if ws.sheet_state == \"visible\":\n            assert ws.protection.sheet, f\"{ws.title} is not protected\"\n    assert wb.security and wb.security.lockStructure, \"structure not locked\"\n    for name in expect_hidden:\n        assert wb[name].sheet_state != \"visible\", f\"{name} is visible\"\n    return True\n",[390,2659,2660,2670,2674,2689,2699,2710,2722,2744,2761,2772,2801],{"__ignoreMap":388},[393,2661,2662,2664,2666,2668],{"class":311,"line":395},[393,2663,399],{"class":398},[393,2665,403],{"class":402},[393,2667,406],{"class":398},[393,2669,1104],{"class":402},[393,2671,2672],{"class":311,"line":412},[393,2673,429],{"emptyLinePlaceholder":428},[393,2675,2676,2678,2681,2684,2686],{"class":311,"line":425},[393,2677,1280],{"class":398},[393,2679,2680],{"class":1283}," assert_hardened",[393,2682,2683],{"class":402},"(path, expect_hidden",[393,2685,438],{"class":398},[393,2687,2688],{"class":402},"()):\n",[393,2690,2691,2694,2696],{"class":311,"line":432},[393,2692,2693],{"class":402},"    wb ",[393,2695,438],{"class":398},[393,2697,2698],{"class":402}," load_workbook(path)\n",[393,2700,2701,2703,2705,2707],{"class":311,"line":444},[393,2702,873],{"class":398},[393,2704,1349],{"class":402},[393,2706,543],{"class":398},[393,2708,2709],{"class":402}," wb.worksheets:\n",[393,2711,2712,2714,2716,2718,2720],{"class":311,"line":455},[393,2713,1359],{"class":398},[393,2715,1362],{"class":402},[393,2717,1365],{"class":398},[393,2719,1368],{"class":463},[393,2721,2334],{"class":402},[393,2723,2724,2727,2730,2732,2734,2736,2739,2741],{"class":311,"line":467},[393,2725,2726],{"class":398},"            assert",[393,2728,2729],{"class":402}," ws.protection.sheet, ",[393,2731,659],{"class":398},[393,2733,675],{"class":463},[393,2735,666],{"class":665},[393,2737,2738],{"class":402},"ws.title",[393,2740,672],{"class":665},[393,2742,2743],{"class":463}," is not protected\"\n",[393,2745,2746,2749,2752,2755,2758],{"class":311,"line":472},[393,2747,2748],{"class":398},"    assert",[393,2750,2751],{"class":402}," wb.security ",[393,2753,2754],{"class":398},"and",[393,2756,2757],{"class":402}," wb.security.lockStructure, ",[393,2759,2760],{"class":463},"\"structure not locked\"\n",[393,2762,2763,2765,2767,2769],{"class":311,"line":489},[393,2764,873],{"class":398},[393,2766,1422],{"class":402},[393,2768,543],{"class":398},[393,2770,2771],{"class":402}," expect_hidden:\n",[393,2773,2774,2777,2780,2783,2785,2787,2789,2791,2793,2796,2798],{"class":311,"line":504},[393,2775,2776],{"class":398},"        assert",[393,2778,2779],{"class":402}," wb[name].sheet_state ",[393,2781,2782],{"class":398},"!=",[393,2784,1368],{"class":463},[393,2786,609],{"class":402},[393,2788,659],{"class":398},[393,2790,675],{"class":463},[393,2792,666],{"class":665},[393,2794,2795],{"class":402},"name",[393,2797,672],{"class":665},[393,2799,2800],{"class":463}," is visible\"\n",[393,2802,2803,2805],{"class":311,"line":519},[393,2804,1442],{"class":398},[393,2806,928],{"class":549},[10,2808,2809,2810,2814],{},"The same discipline applies to template-based reports, where the template already carries protection and your script only fills cells — the reason ",[14,2811,2813],{"href":2812},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fpopulate-excel-template-without-losing-formatting\u002F","populating an Excel template without losing formatting"," insists on cell-level writes rather than sheet replacement.",[154,2816,2818],{"id":2817},"putting-the-layers-together","Putting the layers together",[10,2820,2821],{},"A realistic report applies several layers in one pass, and the order matters: unlock before protecting, hide before locking the structure.",[383,2823,2825],{"className":385,"code":2824,"language":387,"meta":388,"style":388},"import os\nfrom openpyxl import load_workbook\nfrom openpyxl.styles import Protection\n\ndef harden(path, dest, input_ranges=(), hide=(), structure=True):\n    \"\"\"Apply the standard protection layers to a generated report.\"\"\"\n    wb = load_workbook(path)\n\n    # 1. Unlock the ranges readers are meant to fill in.\n    for sheet_name, ref in input_ranges:\n        for row in wb[sheet_name][ref]:\n            for cell in row:\n                cell.protection = Protection(locked=False)\n\n    # 2. Tuck away the working sheets.\n    for name in hide:\n        if name in wb.sheetnames:\n            wb[name].sheet_state = \"hidden\"\n\n    # 3. Protect each visible sheet, keeping sort and filter usable.\n    for ws in wb.worksheets:\n        ws.protection.sheet = True\n        ws.protection.autoFilter = False\n        ws.protection.sort = False\n        ws.protection.password = os.environ.get(\"SHEET_PASSWORD\", \"\")\n        ws.protection.enable()\n\n    # 4. Freeze the workbook's shape.\n    if structure:\n        wb.security.lockStructure = True\n\n    wb.save(dest)\n    return dest\n\nharden(\n    \"report.xlsx\",\n    \"report_final.xlsx\",\n    input_ranges=[(\"Forecast\", \"B2:B10\")],\n    hide=[\"Lookups\", \"_raw\"],\n)\n",[390,2826,2827,2833,2843,2854,2858,2884,2889,2897,2901,2906,2918,2930,2941,2958,2962,2967,2978,2989,2999,3003,3008,3018,3027,3036,3045,3065,3070,3074,3079,3086,3095,3099,3104,3111,3115,3121,3129,3137,3158,3178],{"__ignoreMap":388},[393,2828,2829,2831],{"class":311,"line":395},[393,2830,406],{"class":398},[393,2832,1496],{"class":402},[393,2834,2835,2837,2839,2841],{"class":311,"line":412},[393,2836,399],{"class":398},[393,2838,403],{"class":402},[393,2840,406],{"class":398},[393,2842,1104],{"class":402},[393,2844,2845,2847,2849,2851],{"class":311,"line":425},[393,2846,399],{"class":398},[393,2848,417],{"class":402},[393,2850,406],{"class":398},[393,2852,2853],{"class":402}," Protection\n",[393,2855,2856],{"class":311,"line":432},[393,2857,429],{"emptyLinePlaceholder":428},[393,2859,2860,2862,2865,2868,2870,2873,2875,2878,2880,2882],{"class":311,"line":444},[393,2861,1280],{"class":398},[393,2863,2864],{"class":1283}," harden",[393,2866,2867],{"class":402},"(path, dest, input_ranges",[393,2869,438],{"class":398},[393,2871,2872],{"class":402},"(), hide",[393,2874,438],{"class":398},[393,2876,2877],{"class":402},"(), structure",[393,2879,438],{"class":398},[393,2881,573],{"class":549},[393,2883,650],{"class":402},[393,2885,2886],{"class":311,"line":455},[393,2887,2888],{"class":463},"    \"\"\"Apply the standard protection layers to a generated report.\"\"\"\n",[393,2890,2891,2893,2895],{"class":311,"line":467},[393,2892,2693],{"class":402},[393,2894,438],{"class":398},[393,2896,2698],{"class":402},[393,2898,2899],{"class":311,"line":472},[393,2900,429],{"emptyLinePlaceholder":428},[393,2902,2903],{"class":311,"line":489},[393,2904,2905],{"class":789},"    # 1. Unlock the ranges readers are meant to fill in.\n",[393,2907,2908,2910,2913,2915],{"class":311,"line":504},[393,2909,873],{"class":398},[393,2911,2912],{"class":402}," sheet_name, ref ",[393,2914,543],{"class":398},[393,2916,2917],{"class":402}," input_ranges:\n",[393,2919,2920,2923,2925,2927],{"class":311,"line":519},[393,2921,2922],{"class":398},"        for",[393,2924,824],{"class":402},[393,2926,543],{"class":398},[393,2928,2929],{"class":402}," wb[sheet_name][ref]:\n",[393,2931,2932,2935,2937,2939],{"class":311,"line":534},[393,2933,2934],{"class":398},"            for",[393,2936,540],{"class":402},[393,2938,543],{"class":398},[393,2940,880],{"class":402},[393,2942,2943,2946,2948,2950,2952,2954,2956],{"class":311,"line":556},[393,2944,2945],{"class":402},"                cell.protection ",[393,2947,438],{"class":398},[393,2949,891],{"class":402},[393,2951,894],{"class":567},[393,2953,438],{"class":398},[393,2955,899],{"class":549},[393,2957,576],{"class":402},[393,2959,2960],{"class":311,"line":579},[393,2961,429],{"emptyLinePlaceholder":428},[393,2963,2964],{"class":311,"line":584},[393,2965,2966],{"class":789},"    # 2. Tuck away the working sheets.\n",[393,2968,2969,2971,2973,2975],{"class":311,"line":600},[393,2970,873],{"class":398},[393,2972,1422],{"class":402},[393,2974,543],{"class":398},[393,2976,2977],{"class":402}," hide:\n",[393,2979,2980,2982,2984,2986],{"class":311,"line":647},[393,2981,1359],{"class":398},[393,2983,1422],{"class":402},[393,2985,543],{"class":398},[393,2987,2988],{"class":402}," wb.sheetnames:\n",[393,2990,2991,2994,2996],{"class":311,"line":653},[393,2992,2993],{"class":402},"            wb[name].sheet_state ",[393,2995,438],{"class":398},[393,2997,2998],{"class":463}," \"hidden\"\n",[393,3000,3001],{"class":311,"line":685},[393,3002,429],{"emptyLinePlaceholder":428},[393,3004,3005],{"class":311,"line":710},[393,3006,3007],{"class":789},"    # 3. Protect each visible sheet, keeping sort and filter usable.\n",[393,3009,3010,3012,3014,3016],{"class":311,"line":735},[393,3011,873],{"class":398},[393,3013,1349],{"class":402},[393,3015,543],{"class":398},[393,3017,2709],{"class":402},[393,3019,3020,3023,3025],{"class":311,"line":781},[393,3021,3022],{"class":402},"        ws.protection.sheet ",[393,3024,438],{"class":398},[393,3026,928],{"class":549},[393,3028,3029,3032,3034],{"class":311,"line":786},[393,3030,3031],{"class":402},"        ws.protection.autoFilter ",[393,3033,438],{"class":398},[393,3035,1006],{"class":549},[393,3037,3038,3041,3043],{"class":311,"line":793},[393,3039,3040],{"class":402},"        ws.protection.sort ",[393,3042,438],{"class":398},[393,3044,1006],{"class":549},[393,3046,3047,3050,3052,3055,3058,3060,3063],{"class":311,"line":819},[393,3048,3049],{"class":402},"        ws.protection.password ",[393,3051,438],{"class":398},[393,3053,3054],{"class":402}," os.environ.get(",[393,3056,3057],{"class":463},"\"SHEET_PASSWORD\"",[393,3059,609],{"class":402},[393,3061,3062],{"class":463},"\"\"",[393,3064,576],{"class":402},[393,3066,3067],{"class":311,"line":870},[393,3068,3069],{"class":402},"        ws.protection.enable()\n",[393,3071,3072],{"class":311,"line":883},[393,3073,429],{"emptyLinePlaceholder":428},[393,3075,3076],{"class":311,"line":904},[393,3077,3078],{"class":789},"    # 4. Freeze the workbook's shape.\n",[393,3080,3081,3083],{"class":311,"line":915},[393,3082,1393],{"class":398},[393,3084,3085],{"class":402}," structure:\n",[393,3087,3088,3091,3093],{"class":311,"line":920},[393,3089,3090],{"class":402},"        wb.security.lockStructure ",[393,3092,438],{"class":398},[393,3094,928],{"class":549},[393,3096,3097],{"class":311,"line":931},[393,3098,429],{"emptyLinePlaceholder":428},[393,3100,3101],{"class":311,"line":945},[393,3102,3103],{"class":402},"    wb.save(dest)\n",[393,3105,3106,3108],{"class":311,"line":951},[393,3107,1442],{"class":398},[393,3109,3110],{"class":402}," dest\n",[393,3112,3113],{"class":311,"line":956},[393,3114,429],{"emptyLinePlaceholder":428},[393,3116,3118],{"class":311,"line":3117},35,[393,3119,3120],{"class":402},"harden(\n",[393,3122,3124,3127],{"class":311,"line":3123},36,[393,3125,3126],{"class":463},"    \"report.xlsx\"",[393,3128,1952],{"class":402},[393,3130,3132,3135],{"class":311,"line":3131},37,[393,3133,3134],{"class":463},"    \"report_final.xlsx\"",[393,3136,1952],{"class":402},[393,3138,3140,3143,3145,3148,3150,3152,3155],{"class":311,"line":3139},38,[393,3141,3142],{"class":567},"    input_ranges",[393,3144,438],{"class":398},[393,3146,3147],{"class":402},"[(",[393,3149,1917],{"class":463},[393,3151,609],{"class":402},[393,3153,3154],{"class":463},"\"B2:B10\"",[393,3156,3157],{"class":402},")],\n",[393,3159,3161,3164,3166,3169,3171,3173,3175],{"class":311,"line":3160},39,[393,3162,3163],{"class":567},"    hide",[393,3165,438],{"class":398},[393,3167,3168],{"class":402},"[",[393,3170,1220],{"class":463},[393,3172,609],{"class":402},[393,3174,1238],{"class":463},[393,3176,3177],{"class":402},"],\n",[393,3179,3181],{"class":311,"line":3180},40,[393,3182,576],{"class":402},[10,3184,3185,3186,18],{},"Because every step here is deterministic, it belongs in the test suite alongside the rest of the output checks — assert that the protection flag is set, that the input range is unlocked, and that the working sheets are hidden, using the approach in ",[14,3187,3189],{"href":3188},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002F","testing Excel output with pytest",[154,3191,3193],{"id":3192},"key-takeaways","Key takeaways",[3195,3196,3197,3204,3210,3226,3234,3244,3250],"ul",{},[3198,3199,3200,3203],"li",{},[164,3201,3202],{},"Sheet and workbook protection are guard rails, not security."," They prevent accidents; they do not prevent reading.",[3198,3205,3206,3209],{},[164,3207,3208],{},"Unlock, then protect."," Every cell is locked by default and the lock is inert until sheet protection is enabled.",[3198,3211,3212,3215,3216,3218,3219,3221,3222,3225],{},[164,3213,3214],{},"The protection flags are inverted."," ",[390,3217,899],{}," permits, ",[390,3220,573],{}," blocks — so set ",[390,3223,3224],{},"autoFilter = False"," to keep filtering usable.",[3198,3227,3228,3233],{},[164,3229,3230,3232],{},[390,3231,1266],{}," is tidiness."," Any library reads a very hidden sheet without effort.",[3198,3235,3236,3239,3240,3243],{},[164,3237,3238],{},"File encryption is the only real confidentiality control",", and it needs ",[390,3241,3242],{},"msoffcrypto-tool"," rather than openpyxl.",[3198,3245,3246,3249],{},[164,3247,3248],{},"Keep passwords out of source",", and never send an encrypted file and its password in the same message.",[3198,3251,3252,3255],{},[164,3253,3254],{},"Comments and document properties carry the \"why\""," with the file, where a covering email does not.",[154,3257,3259],{"id":3258},"frequently-asked-questions","Frequently asked questions",[10,3261,3262,3265],{},[164,3263,3264],{},"Does openpyxl's sheet protection actually stop anyone?","\nNo. Sheet and workbook protection are interface guards — they stop accidental edits in Excel and nothing more. The password is stored as a weak hash and any library, including openpyxl, can strip it. For real confidentiality you need file-level encryption.",[10,3267,3268,3271],{},[164,3269,3270],{},"What is the difference between protecting a sheet and encrypting the file?","\nSheet protection restricts what Excel's interface will let a reader change once the file is open; anyone can still read every value. Encryption makes the file unreadable without the password, so nothing can be opened at all until it is supplied.",[10,3273,3274,3277],{},[164,3275,3276],{},"Why are all my cells still locked after I protect the sheet?","\nEvery cell is locked by default. The lock only takes effect when sheet protection is enabled, so the pattern is to unlock the input cells first, then protect the sheet.",[10,3279,3280,3287,3288,3290],{},[164,3281,3282,3283,3286],{},"Can Python encrypt an ",[390,3284,3285],{},".xlsx"," file?","\nNot with openpyxl or xlsxwriter. Use the ",[390,3289,3242],{}," package to write an encrypted container, or have headless LibreOffice save the file with a password.",[10,3292,3293,3296],{},[164,3294,3295],{},"Is a very hidden sheet actually hidden?","\nIt is hidden from the normal unhide menu and needs the VBA editor to reveal, which is enough to stop casual browsing. It is not a security control — the data is plainly readable by any library that opens the file.",[10,3298,3299,3302],{},[164,3300,3301],{},"Should I put the password in my script?","\nNo. Read it from an environment variable or a secrets manager. A password committed to a repository is available to everyone with repository access, which is usually more people than the ones meant to open the report.",[154,3304,3306],{"id":3305},"related","Related",[3195,3308,3309,3315,3322,3328,3334,3340,3347],{},[3198,3310,3311,3312,3314],{},"Up to the parent: ",[14,3313,17],{"href":16}," — the section this topic belongs to.",[3198,3316,3317,3321],{},[14,3318,3320],{"href":3319},"\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, end to end.",[3198,3323,3324,3327],{},[14,3325,3326],{"href":1076},"Lock Cells and Protect a Sheet with openpyxl"," — the unlock-then-protect pattern in depth.",[3198,3329,3330,3333],{},[14,3331,3332],{"href":1450},"Hide Sheets, Rows and Columns with openpyxl"," — tidying a generated workbook.",[3198,3335,3336,3339],{},[14,3337,3338],{"href":2012},"Add Comments and Notes to Excel Cells with Python"," — annotating the numbers.",[3198,3341,3342,3346],{},[14,3343,3345],{"href":3344},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002F","Styling Excel Cells with openpyxl"," — the sibling topic for visual formatting.",[3198,3348,3349,3353],{},[14,3350,3352],{"href":3351},"\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 will accept.",[3355,3356,3357],"style",{},"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 .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}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 .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_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}",{"title":388,"searchDepth":412,"depth":412,"links":3359},[3360,3361,3362,3363,3364,3365,3366,3367,3368,3369,3370,3371],{"id":156,"depth":412,"text":157},{"id":250,"depth":412,"text":251},{"id":1080,"depth":412,"text":1081},{"id":1172,"depth":412,"text":1173},{"id":1454,"depth":412,"text":1455},{"id":1858,"depth":412,"text":1859},{"id":2172,"depth":412,"text":2173},{"id":2529,"depth":412,"text":2530},{"id":2817,"depth":412,"text":2818},{"id":3192,"depth":412,"text":3193},{"id":3258,"depth":412,"text":3259},{"id":3305,"depth":412,"text":3306},"2026-08-15","Control what readers can change in a generated Excel report: sheet and workbook protection, locked cells, hidden sheets, file encryption, comments and document properties.","md",[3376,3378,3380,3382,3385,3387],{"q":3264,"a":3377},"No. Sheet and workbook protection are interface guards — they stop accidental edits in Excel and nothing more. The password is stored as a weak hash and any library, including openpyxl, can strip it. For real confidentiality you need file-level encryption.",{"q":3270,"a":3379},"Sheet protection restricts what Excel's interface will let a reader change once the file is open; anyone can still read every value. Encryption makes the file unreadable without the password, so nothing can be opened at all until it is supplied.",{"q":3276,"a":3381},"Every cell is locked by default. The lock only takes effect when sheet protection is enabled, so the pattern is to unlock the input cells first, then protect the sheet.",{"q":3383,"a":3384},"Can Python encrypt an .xlsx file?","Not with openpyxl or xlsxwriter. Use the msoffcrypto-tool package to write an encrypted container, or have headless LibreOffice save the file with a password.",{"q":3295,"a":3386},"It is hidden from the normal unhide menu and needs the VBA editor to reveal, which is enough to stop casual browsing. It is not a security control — the data is plainly readable by any library that opens the file.",{"q":3301,"a":3388},"No. Read it from an environment variable or a secrets manager. A password committed to a repository is available to everyone with repository access, which is usually more people than the ones meant to open the report.",{},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks",{"title":3392,"description":3393},"Protect and Share Excel Workbooks from Python","Sheet protection, cell locking, hidden sheets, real file encryption, cell comments and document metadata — everything Python can set before a report leaves your machine.","protecting-and-sharing-excel-workbooks","formatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Findex","guide","fA8w4z4L_RKizhq5bUlcWeQRo37OksQMaU9zu_I1HOo",[3399,3403],{"title":3400,"path":3401,"stem":3402,"children":-1},"Resize and Position Images in Excel with openpyxl","\u002Fformatting-and-charting-excel-reports-with-python\u002Finserting-images-and-logos-into-excel\u002Fresize-and-position-images-in-excel-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Finserting-images-and-logos-into-excel\u002Fresize-and-position-images-in-excel-with-openpyxl\u002Findex",{"title":3338,"path":3404,"stem":3405,"children":-1},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fadd-comments-and-notes-to-excel-cells-with-python","formatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fadd-comments-and-notes-to-excel-cells-with-python\u002Findex",1786800026511]