[{"data":1,"prerenderedAt":1936},["ShallowReactive",2],{"doc:\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fopen-a-password-protected-excel-file-with-python":3,"surround:\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fopen-a-password-protected-excel-file-with-python":1927},{"id":4,"title":5,"body":6,"dateModified":1904,"datePublished":1904,"description":1905,"extension":1906,"faq":1907,"meta":1918,"navigation":195,"path":1919,"seo":1920,"slug":1923,"stem":1924,"type":1925,"__hash__":1926},"docs\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fopen-a-password-protected-excel-file-with-python\u002Findex.md","Open a Password-Protected Excel File with Python",{"type":7,"value":8,"toc":1887},"minimark",[9,24,120,125,155,160,164,167,407,418,466,470,473,580,587,591,594,809,812,924,928,931,987,990,994,1233,1236,1240,1248,1252,1255,1317,1324,1328,1331,1417,1424,1428,1431,1728,1735,1739,1781,1785,1796,1800,1810,1814,1820,1826,1832,1838,1848,1852,1883],[10,11,12,13,17,18,23],"p",{},"A workbook saved with Excel's \"Password to open\" is encrypted, and every Python reader fails on it in a way that hides the cause: openpyxl reports that the file is not a zip, and pandas cannot determine the format. Neither message mentions encryption. The fix is a small library, ",[14,15,16],"code",{},"msoffcrypto-tool",", which decrypts the container into a buffer that the normal readers then handle unchanged. This guide decrypts one file and a folder of them, handles wrong passwords cleanly, and draws the line between encryption and the sheet protection it is often confused with. It belongs to ",[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,34,35,34,39,34,43,34,50,34,57,34,62,34,70,34,78,34,83,34,86,34,90,34,94,34,99,34,103,34,108,34,110,34,113,34,116],"svg",{"viewBox":27,"role":28,"ariaLabelledBy":29,"xmlns":32,"style":33},"0 0 760 232","img",[30,31],"pw-t","pw-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[36,37,38],"title",{"id":30},"Two different things called a password",[40,41,42],"desc",{"id":31},"Encryption wraps the whole workbook in an OLE2 container that must be decrypted before any reader sees a zip, while sheet protection is a flag inside a perfectly readable file.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","232","#ffffff",[51,52,56],"text",{"x":53,"y":54,"style":55},"196","28","font-size:12.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","encrypted (password to open)",[51,58,61],{"x":59,"y":54,"style":60},"566","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","protected (password to edit)",[63,64],"line",{"x1":65,"y1":66,"x2":65,"y2":67,"stroke":68,"style":69},"380","40","216","var(--line,#cdd5e6)","stroke-width:1px",[44,71],{"x":72,"y":73,"width":74,"height":75,"rx":76,"fill":77,"stroke":68},"30","46","332","44","9","#fee8f2",[51,79,82],{"x":53,"y":80,"style":81},"74","font-size:12px;fill:var(--accent-ink,#be185d);text-anchor:middle","OLE2 container, encrypted payload",[44,84],{"x":72,"y":85,"width":74,"height":75,"rx":76,"fill":77,"stroke":68},"98",[51,87,89],{"x":53,"y":88,"style":81},"126","openpyxl: \"not a zip file\"",[44,91],{"x":72,"y":92,"width":74,"height":75,"rx":76,"fill":93,"stroke":68},"150","#fdefd8",[51,95,98],{"x":53,"y":96,"style":97},"178","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","decrypt first, then read",[44,100],{"x":101,"y":73,"width":74,"height":75,"rx":76,"fill":102,"stroke":68},"398","#d9f4f1",[51,104,107],{"x":105,"y":80,"style":106},"564","font-size:12px;fill:var(--teal-ink,#0b6157);text-anchor:middle","ordinary zip, readable bytes",[44,109],{"x":101,"y":85,"width":74,"height":75,"rx":76,"fill":102,"stroke":68},[51,111,112],{"x":105,"y":88,"style":106},"openpyxl opens it with no password",[44,114],{"x":101,"y":92,"width":74,"height":75,"rx":76,"fill":115,"stroke":68},"#ebebfd",[51,117,119],{"x":105,"y":96,"style":118},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","a flag, not a lock, for code",[121,122,124],"h2",{"id":123},"prerequisites","Prerequisites",[126,127,132],"pre",{"className":128,"code":129,"language":130,"meta":131,"style":131},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install msoffcrypto-tool pandas openpyxl\n","bash","",[14,133,134],{"__ignoreMap":131},[135,136,138,142,146,149,152],"span",{"class":63,"line":137},1,[135,139,141],{"class":140},"sMTad","pip",[135,143,145],{"class":144},"srMev"," install",[135,147,148],{"class":144}," msoffcrypto-tool",[135,150,151],{"class":144}," pandas",[135,153,154],{"class":144}," openpyxl\n",[10,156,157,159],{},[14,158,16],{}," handles the encryption formats Office uses, including the modern AES-based scheme and the older ones.",[121,161,163],{"id":162},"decrypt-into-memory-and-read","Decrypt into memory and read",[10,165,166],{},"The whole operation is one function, and the plaintext workbook never reaches disk:",[126,168,172],{"className":169,"code":170,"language":171,"meta":131,"style":131},"language-python shiki shiki-themes github-light github-dark-high-contrast","\"\"\"Read an encrypted workbook without writing a decrypted copy.\"\"\"\nimport io\n\nimport msoffcrypto\nimport pandas as pd\n\ndef read_encrypted(path: str, password: str, **kwargs) -> pd.DataFrame:\n    decrypted = io.BytesIO()\n    with open(path, \"rb\") as fh:\n        office_file = msoffcrypto.OfficeFile(fh)\n        office_file.load_key(password=password)\n        office_file.decrypt(decrypted)\n\n    decrypted.seek(0)\n    kwargs.setdefault(\"engine\", \"openpyxl\")\n    return pd.read_excel(decrypted, **kwargs)\n\ndf = read_encrypted(\"confidential.xlsx\", password=\"hunter2\")\nprint(df.head())\n","python",[14,173,174,179,190,197,205,219,224,255,267,290,301,316,322,327,338,354,368,373,398],{"__ignoreMap":131},[135,175,176],{"class":63,"line":137},[135,177,178],{"class":144},"\"\"\"Read an encrypted workbook without writing a decrypted copy.\"\"\"\n",[135,180,182,186],{"class":63,"line":181},2,[135,183,185],{"class":184},"s-kum","import",[135,187,189],{"class":188},"skGVy"," io\n",[135,191,193],{"class":63,"line":192},3,[135,194,196],{"emptyLinePlaceholder":195},true,"\n",[135,198,200,202],{"class":63,"line":199},4,[135,201,185],{"class":184},[135,203,204],{"class":188}," msoffcrypto\n",[135,206,208,210,213,216],{"class":63,"line":207},5,[135,209,185],{"class":184},[135,211,212],{"class":188}," pandas ",[135,214,215],{"class":184},"as",[135,217,218],{"class":188}," pd\n",[135,220,222],{"class":63,"line":221},6,[135,223,196],{"emptyLinePlaceholder":195},[135,225,227,230,234,237,241,244,246,249,252],{"class":63,"line":226},7,[135,228,229],{"class":184},"def",[135,231,233],{"class":232},"s_Opv"," read_encrypted",[135,235,236],{"class":188},"(path: ",[135,238,240],{"class":239},"sP0c6","str",[135,242,243],{"class":188},", password: ",[135,245,240],{"class":239},[135,247,248],{"class":188},", ",[135,250,251],{"class":184},"**",[135,253,254],{"class":188},"kwargs) -> pd.DataFrame:\n",[135,256,258,261,264],{"class":63,"line":257},8,[135,259,260],{"class":188},"    decrypted ",[135,262,263],{"class":184},"=",[135,265,266],{"class":188}," io.BytesIO()\n",[135,268,270,273,276,279,282,285,287],{"class":63,"line":269},9,[135,271,272],{"class":184},"    with",[135,274,275],{"class":239}," open",[135,277,278],{"class":188},"(path, ",[135,280,281],{"class":144},"\"rb\"",[135,283,284],{"class":188},") ",[135,286,215],{"class":184},[135,288,289],{"class":188}," fh:\n",[135,291,293,296,298],{"class":63,"line":292},10,[135,294,295],{"class":188},"        office_file ",[135,297,263],{"class":184},[135,299,300],{"class":188}," msoffcrypto.OfficeFile(fh)\n",[135,302,304,307,311,313],{"class":63,"line":303},11,[135,305,306],{"class":188},"        office_file.load_key(",[135,308,310],{"class":309},"sa561","password",[135,312,263],{"class":184},[135,314,315],{"class":188},"password)\n",[135,317,319],{"class":63,"line":318},12,[135,320,321],{"class":188},"        office_file.decrypt(decrypted)\n",[135,323,325],{"class":63,"line":324},13,[135,326,196],{"emptyLinePlaceholder":195},[135,328,330,333,335],{"class":63,"line":329},14,[135,331,332],{"class":188},"    decrypted.seek(",[135,334,46],{"class":239},[135,336,337],{"class":188},")\n",[135,339,341,344,347,349,352],{"class":63,"line":340},15,[135,342,343],{"class":188},"    kwargs.setdefault(",[135,345,346],{"class":144},"\"engine\"",[135,348,248],{"class":188},[135,350,351],{"class":144},"\"openpyxl\"",[135,353,337],{"class":188},[135,355,357,360,363,365],{"class":63,"line":356},16,[135,358,359],{"class":184},"    return",[135,361,362],{"class":188}," pd.read_excel(decrypted, ",[135,364,251],{"class":184},[135,366,367],{"class":188},"kwargs)\n",[135,369,371],{"class":63,"line":370},17,[135,372,196],{"emptyLinePlaceholder":195},[135,374,376,379,381,384,387,389,391,393,396],{"class":63,"line":375},18,[135,377,378],{"class":188},"df ",[135,380,263],{"class":184},[135,382,383],{"class":188}," read_encrypted(",[135,385,386],{"class":144},"\"confidential.xlsx\"",[135,388,248],{"class":188},[135,390,310],{"class":309},[135,392,263],{"class":184},[135,394,395],{"class":144},"\"hunter2\"",[135,397,337],{"class":188},[135,399,401,404],{"class":63,"line":400},19,[135,402,403],{"class":239},"print",[135,405,406],{"class":188},"(df.head())\n",[10,408,409,410,413,414,417],{},"After ",[14,411,412],{},"decrypt()"," the buffer holds an ordinary ",[14,415,416],{},".xlsx",", so everything downstream is unchanged — openpyxl works the same way:",[126,419,421],{"className":169,"code":420,"language":171,"meta":131,"style":131},"from openpyxl import load_workbook\n\ndecrypted.seek(0)\nwb = load_workbook(decrypted)\nprint(wb.sheetnames)\n",[14,422,423,436,440,449,459],{"__ignoreMap":131},[135,424,425,428,431,433],{"class":63,"line":137},[135,426,427],{"class":184},"from",[135,429,430],{"class":188}," openpyxl ",[135,432,185],{"class":184},[135,434,435],{"class":188}," load_workbook\n",[135,437,438],{"class":63,"line":181},[135,439,196],{"emptyLinePlaceholder":195},[135,441,442,445,447],{"class":63,"line":192},[135,443,444],{"class":188},"decrypted.seek(",[135,446,46],{"class":239},[135,448,337],{"class":188},[135,450,451,454,456],{"class":63,"line":199},[135,452,453],{"class":188},"wb ",[135,455,263],{"class":184},[135,457,458],{"class":188}," load_workbook(decrypted)\n",[135,460,461,463],{"class":63,"line":207},[135,462,403],{"class":239},[135,464,465],{"class":188},"(wb.sheetnames)\n",[121,467,469],{"id":468},"detect-encryption-before-trying-to-read","Detect encryption before trying to read",[10,471,472],{},"Rather than catching a confusing error, ask the question directly:",[126,474,476],{"className":169,"code":475,"language":171,"meta":131,"style":131},"import msoffcrypto\n\ndef is_encrypted(path: str) -> bool:\n    with open(path, \"rb\") as fh:\n        try:\n            return msoffcrypto.OfficeFile(fh).is_encrypted()\n        except Exception:          # not an Office container at all\n            return False\n\nprint(is_encrypted(\"confidential.xlsx\"))   # True\n",[14,477,478,484,488,508,524,531,539,554,561,565],{"__ignoreMap":131},[135,479,480,482],{"class":63,"line":137},[135,481,185],{"class":184},[135,483,204],{"class":188},[135,485,486],{"class":63,"line":181},[135,487,196],{"emptyLinePlaceholder":195},[135,489,490,492,495,497,499,502,505],{"class":63,"line":192},[135,491,229],{"class":184},[135,493,494],{"class":232}," is_encrypted",[135,496,236],{"class":188},[135,498,240],{"class":239},[135,500,501],{"class":188},") -> ",[135,503,504],{"class":239},"bool",[135,506,507],{"class":188},":\n",[135,509,510,512,514,516,518,520,522],{"class":63,"line":199},[135,511,272],{"class":184},[135,513,275],{"class":239},[135,515,278],{"class":188},[135,517,281],{"class":144},[135,519,284],{"class":188},[135,521,215],{"class":184},[135,523,289],{"class":188},[135,525,526,529],{"class":63,"line":207},[135,527,528],{"class":184},"        try",[135,530,507],{"class":188},[135,532,533,536],{"class":63,"line":221},[135,534,535],{"class":184},"            return",[135,537,538],{"class":188}," msoffcrypto.OfficeFile(fh).is_encrypted()\n",[135,540,541,544,547,550],{"class":63,"line":226},[135,542,543],{"class":184},"        except",[135,545,546],{"class":239}," Exception",[135,548,549],{"class":188},":          ",[135,551,553],{"class":552},"s-wDw","# not an Office container at all\n",[135,555,556,558],{"class":63,"line":257},[135,557,535],{"class":184},[135,559,560],{"class":239}," False\n",[135,562,563],{"class":63,"line":269},[135,564,196],{"emptyLinePlaceholder":195},[135,566,567,569,572,574,577],{"class":63,"line":292},[135,568,403],{"class":239},[135,570,571],{"class":188},"(is_encrypted(",[135,573,386],{"class":144},[135,575,576],{"class":188},"))   ",[135,578,579],{"class":552},"# True\n",[10,581,582,583,23],{},"That lets an ingest step branch cleanly: decrypt what is encrypted, read the rest directly, and report anything that is neither. The related identification problem is covered in ",[19,584,586],{"href":585},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-badzipfile-error-when-reading-excel-in-python\u002F","Fix BadZipFile when reading an Excel file in Python",[121,588,590],{"id":589},"handle-a-wrong-password-properly","Handle a wrong password properly",[10,592,593],{},"A bad password raises, and the message deserves improving before it reaches a log:",[126,595,597],{"className":169,"code":596,"language":171,"meta":131,"style":131},"import io\n\nimport msoffcrypto\n\nclass DecryptionError(RuntimeError):\n    pass\n\ndef decrypt(path: str, password: str) -> io.BytesIO:\n    out = io.BytesIO()\n    with open(path, \"rb\") as fh:\n        office_file = msoffcrypto.OfficeFile(fh)\n        if not office_file.is_encrypted():\n            return io.BytesIO(open(path, \"rb\").read())\n        try:\n            office_file.load_key(password=password)\n            office_file.decrypt(out)\n        except msoffcrypto.exceptions.InvalidKeyError as exc:\n            raise DecryptionError(f\"{path}: wrong password\") from exc\n    out.seek(0)\n    return out\n",[14,598,599,605,609,615,619,636,641,645,663,672,688,696,707,724,730,741,746,758,792,801],{"__ignoreMap":131},[135,600,601,603],{"class":63,"line":137},[135,602,185],{"class":184},[135,604,189],{"class":188},[135,606,607],{"class":63,"line":181},[135,608,196],{"emptyLinePlaceholder":195},[135,610,611,613],{"class":63,"line":192},[135,612,185],{"class":184},[135,614,204],{"class":188},[135,616,617],{"class":63,"line":199},[135,618,196],{"emptyLinePlaceholder":195},[135,620,621,624,627,630,633],{"class":63,"line":207},[135,622,623],{"class":184},"class",[135,625,626],{"class":140}," DecryptionError",[135,628,629],{"class":188},"(",[135,631,632],{"class":239},"RuntimeError",[135,634,635],{"class":188},"):\n",[135,637,638],{"class":63,"line":221},[135,639,640],{"class":184},"    pass\n",[135,642,643],{"class":63,"line":226},[135,644,196],{"emptyLinePlaceholder":195},[135,646,647,649,652,654,656,658,660],{"class":63,"line":257},[135,648,229],{"class":184},[135,650,651],{"class":232}," decrypt",[135,653,236],{"class":188},[135,655,240],{"class":239},[135,657,243],{"class":188},[135,659,240],{"class":239},[135,661,662],{"class":188},") -> io.BytesIO:\n",[135,664,665,668,670],{"class":63,"line":269},[135,666,667],{"class":188},"    out ",[135,669,263],{"class":184},[135,671,266],{"class":188},[135,673,674,676,678,680,682,684,686],{"class":63,"line":292},[135,675,272],{"class":184},[135,677,275],{"class":239},[135,679,278],{"class":188},[135,681,281],{"class":144},[135,683,284],{"class":188},[135,685,215],{"class":184},[135,687,289],{"class":188},[135,689,690,692,694],{"class":63,"line":303},[135,691,295],{"class":188},[135,693,263],{"class":184},[135,695,300],{"class":188},[135,697,698,701,704],{"class":63,"line":318},[135,699,700],{"class":184},"        if",[135,702,703],{"class":184}," not",[135,705,706],{"class":188}," office_file.is_encrypted():\n",[135,708,709,711,714,717,719,721],{"class":63,"line":324},[135,710,535],{"class":184},[135,712,713],{"class":188}," io.BytesIO(",[135,715,716],{"class":239},"open",[135,718,278],{"class":188},[135,720,281],{"class":144},[135,722,723],{"class":188},").read())\n",[135,725,726,728],{"class":63,"line":329},[135,727,528],{"class":184},[135,729,507],{"class":188},[135,731,732,735,737,739],{"class":63,"line":340},[135,733,734],{"class":188},"            office_file.load_key(",[135,736,310],{"class":309},[135,738,263],{"class":184},[135,740,315],{"class":188},[135,742,743],{"class":63,"line":356},[135,744,745],{"class":188},"            office_file.decrypt(out)\n",[135,747,748,750,753,755],{"class":63,"line":370},[135,749,543],{"class":184},[135,751,752],{"class":188}," msoffcrypto.exceptions.InvalidKeyError ",[135,754,215],{"class":184},[135,756,757],{"class":188}," exc:\n",[135,759,760,763,766,769,772,776,779,782,785,787,789],{"class":63,"line":375},[135,761,762],{"class":184},"            raise",[135,764,765],{"class":188}," DecryptionError(",[135,767,768],{"class":184},"f",[135,770,771],{"class":144},"\"",[135,773,775],{"class":774},"sSjpA","{",[135,777,778],{"class":188},"path",[135,780,781],{"class":774},"}",[135,783,784],{"class":144},": wrong password\"",[135,786,284],{"class":188},[135,788,427],{"class":184},[135,790,791],{"class":188}," exc\n",[135,793,794,797,799],{"class":63,"line":400},[135,795,796],{"class":188},"    out.seek(",[135,798,46],{"class":239},[135,800,337],{"class":188},[135,802,804,806],{"class":63,"line":803},20,[135,805,359],{"class":184},[135,807,808],{"class":188}," out\n",[10,810,811],{},"Returning the file unchanged when it is not encrypted makes the function safe to call on everything, which is what an ingest loop wants.",[25,813,34,818,34,821,34,824,34,827,34,832,34,839,34,845,34,852,34,857,34,860,34,864,34,871,34,875,34,880,34,884,34,890,34,896,34,899,34,904,34,908,34,911,34,913,34,916,34,920],{"viewBox":814,"role":28,"ariaLabelledBy":815,"xmlns":32,"style":33},"0 0 760 210",[816,817],"pw2-t","pw2-d",[36,819,820],{"id":816},"One ingest path that handles both kinds of file",[40,822,823],{"id":817},"Each incoming file is tested for encryption; encrypted ones are decrypted into a buffer and plain ones pass through, so a single reader handles the merged stream.",[44,825],{"x":46,"y":46,"width":47,"height":826,"fill":49},"210",[51,828,831],{"x":65,"y":829,"style":830},"26","font-size:13px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","Branch once, read once",[44,833],{"x":829,"y":834,"width":835,"height":836,"rx":837,"fill":838,"stroke":68},"76","146","58","11","#f0f2f5",[51,840,844],{"x":841,"y":842,"style":843},"99","110","font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","incoming file",[63,846],{"x1":847,"y1":848,"x2":849,"y2":848,"stroke":850,"style":851},"172","105","204","var(--brand,#5b5cf0)","stroke-width:2px",[853,854],"polygon",{"points":855,"fill":856},"204,105 194,100 194,110","#5b5cf0",[44,858],{"x":859,"y":834,"width":92,"height":836,"rx":837,"fill":115,"stroke":68},"208",[51,861,863],{"x":862,"y":842,"style":118},"283","is_encrypted()?",[63,865],{"x1":866,"y1":867,"x2":868,"y2":869,"stroke":870,"style":851},"358","92","392","66","var(--accent,#f43f8f)",[853,872],{"points":873,"fill":874},"392,66 385,77 380,69","#be185d",[63,876],{"x1":866,"y1":877,"x2":868,"y2":878,"stroke":879,"style":851},"118","144","var(--teal,#0f9488)",[853,881],{"points":882,"fill":883},"392,144 380,141 385,133","#0f766e",[44,885],{"x":886,"y":66,"width":887,"height":888,"rx":889,"fill":77,"stroke":68},"396","180","52","10",[51,891,895],{"x":892,"y":893,"style":894},"486","72","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","decrypt to BytesIO",[44,897],{"x":886,"y":898,"width":887,"height":888,"rx":889,"fill":102,"stroke":68},"120",[51,900,903],{"x":892,"y":901,"style":902},"152","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","read bytes as they are",[63,905],{"x1":906,"y1":869,"x2":907,"y2":867,"stroke":870,"style":851},"576","610",[853,909],{"points":910,"fill":874},"610,92 598,89 603,81",[63,912],{"x1":906,"y1":835,"x2":907,"y2":898,"stroke":879,"style":851},[853,914],{"points":915,"fill":883},"610,120 603,131 598,123",[44,917],{"x":918,"y":834,"width":919,"height":836,"rx":837,"fill":93,"stroke":68},"614","122",[51,921,923],{"x":922,"y":842,"style":97},"675","one reader",[121,925,927],{"id":926},"keep-the-password-out-of-the-code","Keep the password out of the code",[10,929,930],{},"A password in a source file outlives every job that used it. Read it from the environment, or from whatever secrets manager the deployment already has:",[126,932,934],{"className":169,"code":933,"language":171,"meta":131,"style":131},"import os\n\npassword = os.environ.get(\"REPORT_PASSWORD\")\nif not password:\n    raise SystemExit(\"REPORT_PASSWORD is not set\")\n",[14,935,936,943,947,962,972],{"__ignoreMap":131},[135,937,938,940],{"class":63,"line":137},[135,939,185],{"class":184},[135,941,942],{"class":188}," os\n",[135,944,945],{"class":63,"line":181},[135,946,196],{"emptyLinePlaceholder":195},[135,948,949,952,954,957,960],{"class":63,"line":192},[135,950,951],{"class":188},"password ",[135,953,263],{"class":184},[135,955,956],{"class":188}," os.environ.get(",[135,958,959],{"class":144},"\"REPORT_PASSWORD\"",[135,961,337],{"class":188},[135,963,964,967,969],{"class":63,"line":199},[135,965,966],{"class":184},"if",[135,968,703],{"class":184},[135,970,971],{"class":188}," password:\n",[135,973,974,977,980,982,985],{"class":63,"line":207},[135,975,976],{"class":184},"    raise",[135,978,979],{"class":239}," SystemExit",[135,981,629],{"class":188},[135,983,984],{"class":144},"\"REPORT_PASSWORD is not set\"",[135,986,337],{"class":188},[10,988,989],{},"For a folder of files from different senders, keep a mapping outside the code — an environment variable per sender, or a secrets-manager lookup keyed on the sender's name. Never log the password, and never put it in a filename.",[121,991,993],{"id":992},"decrypt-a-whole-folder","Decrypt a whole folder",[126,995,997],{"className":169,"code":996,"language":171,"meta":131,"style":131},"\"\"\"Decrypt every encrypted workbook into a working directory.\"\"\"\nimport os\nfrom pathlib import Path\n\nimport msoffcrypto\n\nSRC = Path(\"incoming\")\nDST = Path(\"decrypted\")\nDST.mkdir(exist_ok=True)\npassword = os.environ[\"REPORT_PASSWORD\"]\n\nfor src in sorted(SRC.glob(\"*.xlsx\")):\n    with open(src, \"rb\") as fh:\n        office_file = msoffcrypto.OfficeFile(fh)\n        if not office_file.is_encrypted():\n            print(\"plain:\", src.name)\n            continue\n        office_file.load_key(password=password)\n        with open(DST \u002F src.name, \"wb\") as out:\n            office_file.decrypt(out)\n    print(\"decrypted:\", src.name)\n",[14,998,999,1004,1010,1022,1026,1032,1036,1052,1066,1083,1097,1101,1128,1145,1153,1161,1174,1179,1189,1216,1220],{"__ignoreMap":131},[135,1000,1001],{"class":63,"line":137},[135,1002,1003],{"class":144},"\"\"\"Decrypt every encrypted workbook into a working directory.\"\"\"\n",[135,1005,1006,1008],{"class":63,"line":181},[135,1007,185],{"class":184},[135,1009,942],{"class":188},[135,1011,1012,1014,1017,1019],{"class":63,"line":192},[135,1013,427],{"class":184},[135,1015,1016],{"class":188}," pathlib ",[135,1018,185],{"class":184},[135,1020,1021],{"class":188}," Path\n",[135,1023,1024],{"class":63,"line":199},[135,1025,196],{"emptyLinePlaceholder":195},[135,1027,1028,1030],{"class":63,"line":207},[135,1029,185],{"class":184},[135,1031,204],{"class":188},[135,1033,1034],{"class":63,"line":221},[135,1035,196],{"emptyLinePlaceholder":195},[135,1037,1038,1041,1044,1047,1050],{"class":63,"line":226},[135,1039,1040],{"class":239},"SRC",[135,1042,1043],{"class":184}," =",[135,1045,1046],{"class":188}," Path(",[135,1048,1049],{"class":144},"\"incoming\"",[135,1051,337],{"class":188},[135,1053,1054,1057,1059,1061,1064],{"class":63,"line":257},[135,1055,1056],{"class":239},"DST",[135,1058,1043],{"class":184},[135,1060,1046],{"class":188},[135,1062,1063],{"class":144},"\"decrypted\"",[135,1065,337],{"class":188},[135,1067,1068,1070,1073,1076,1078,1081],{"class":63,"line":269},[135,1069,1056],{"class":239},[135,1071,1072],{"class":188},".mkdir(",[135,1074,1075],{"class":309},"exist_ok",[135,1077,263],{"class":184},[135,1079,1080],{"class":239},"True",[135,1082,337],{"class":188},[135,1084,1085,1087,1089,1092,1094],{"class":63,"line":292},[135,1086,951],{"class":188},[135,1088,263],{"class":184},[135,1090,1091],{"class":188}," os.environ[",[135,1093,959],{"class":144},[135,1095,1096],{"class":188},"]\n",[135,1098,1099],{"class":63,"line":303},[135,1100,196],{"emptyLinePlaceholder":195},[135,1102,1103,1106,1109,1112,1115,1117,1119,1122,1125],{"class":63,"line":318},[135,1104,1105],{"class":184},"for",[135,1107,1108],{"class":188}," src ",[135,1110,1111],{"class":184},"in",[135,1113,1114],{"class":239}," sorted",[135,1116,629],{"class":188},[135,1118,1040],{"class":239},[135,1120,1121],{"class":188},".glob(",[135,1123,1124],{"class":144},"\"*.xlsx\"",[135,1126,1127],{"class":188},")):\n",[135,1129,1130,1132,1134,1137,1139,1141,1143],{"class":63,"line":324},[135,1131,272],{"class":184},[135,1133,275],{"class":239},[135,1135,1136],{"class":188},"(src, ",[135,1138,281],{"class":144},[135,1140,284],{"class":188},[135,1142,215],{"class":184},[135,1144,289],{"class":188},[135,1146,1147,1149,1151],{"class":63,"line":329},[135,1148,295],{"class":188},[135,1150,263],{"class":184},[135,1152,300],{"class":188},[135,1154,1155,1157,1159],{"class":63,"line":340},[135,1156,700],{"class":184},[135,1158,703],{"class":184},[135,1160,706],{"class":188},[135,1162,1163,1166,1168,1171],{"class":63,"line":356},[135,1164,1165],{"class":239},"            print",[135,1167,629],{"class":188},[135,1169,1170],{"class":144},"\"plain:\"",[135,1172,1173],{"class":188},", src.name)\n",[135,1175,1176],{"class":63,"line":370},[135,1177,1178],{"class":184},"            continue\n",[135,1180,1181,1183,1185,1187],{"class":63,"line":375},[135,1182,306],{"class":188},[135,1184,310],{"class":309},[135,1186,263],{"class":184},[135,1188,315],{"class":188},[135,1190,1191,1194,1196,1198,1200,1203,1206,1209,1211,1213],{"class":63,"line":400},[135,1192,1193],{"class":184},"        with",[135,1195,275],{"class":239},[135,1197,629],{"class":188},[135,1199,1056],{"class":239},[135,1201,1202],{"class":184}," \u002F",[135,1204,1205],{"class":188}," src.name, ",[135,1207,1208],{"class":144},"\"wb\"",[135,1210,284],{"class":188},[135,1212,215],{"class":184},[135,1214,1215],{"class":188}," out:\n",[135,1217,1218],{"class":63,"line":803},[135,1219,745],{"class":188},[135,1221,1223,1226,1228,1231],{"class":63,"line":1222},21,[135,1224,1225],{"class":239},"    print",[135,1227,629],{"class":188},[135,1229,1230],{"class":144},"\"decrypted:\"",[135,1232,1173],{"class":188},[10,1234,1235],{},"If you write decrypted copies to disk, treat that directory as sensitive: restrict its permissions, keep it off shared volumes, and delete the contents when the run finishes.",[121,1237,1239],{"id":1238},"write-an-encrypted-file-back-out","Write an encrypted file back out",[10,1241,1242,1243,1247],{},"msoffcrypto-tool decrypts; it does not encrypt. To produce a protected workbook you need Excel itself, or a library that wraps it — the options and their trade-offs are in ",[19,1244,1246],{"href":1245},"\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",". For most report pipelines the better answer is transport security rather than file encryption: deliver through a link that requires authentication instead of emailing an encrypted attachment and its password separately.",[121,1249,1251],{"id":1250},"do-not-confuse-this-with-sheet-protection","Do not confuse this with sheet protection",[10,1253,1254],{},"Sheet and structure protection are flags inside a readable file. openpyxl opens such a workbook without a password and can read every cell; the protection only stops editing in Excel's UI:",[126,1256,1258],{"className":169,"code":1257,"language":171,"meta":131,"style":131},"from openpyxl import load_workbook\n\nwb = load_workbook(\"protected.xlsx\")     # no password needed\nws = wb[\"Data\"]\nprint(ws.protection.sheet)               # True — but every value is readable\n",[14,1259,1260,1270,1274,1292,1307],{"__ignoreMap":131},[135,1261,1262,1264,1266,1268],{"class":63,"line":137},[135,1263,427],{"class":184},[135,1265,430],{"class":188},[135,1267,185],{"class":184},[135,1269,435],{"class":188},[135,1271,1272],{"class":63,"line":181},[135,1273,196],{"emptyLinePlaceholder":195},[135,1275,1276,1278,1280,1283,1286,1289],{"class":63,"line":192},[135,1277,453],{"class":188},[135,1279,263],{"class":184},[135,1281,1282],{"class":188}," load_workbook(",[135,1284,1285],{"class":144},"\"protected.xlsx\"",[135,1287,1288],{"class":188},")     ",[135,1290,1291],{"class":552},"# no password needed\n",[135,1293,1294,1297,1299,1302,1305],{"class":63,"line":199},[135,1295,1296],{"class":188},"ws ",[135,1298,263],{"class":184},[135,1300,1301],{"class":188}," wb[",[135,1303,1304],{"class":144},"\"Data\"",[135,1306,1096],{"class":188},[135,1308,1309,1311,1314],{"class":63,"line":207},[135,1310,403],{"class":239},[135,1312,1313],{"class":188},"(ws.protection.sheet)               ",[135,1315,1316],{"class":552},"# True — but every value is readable\n",[10,1318,1319,1320,23],{},"That distinction matters when someone asks for a \"password-protected\" report: if the intent is confidentiality, protection is not enough, and if the intent is preventing accidental edits, encryption is too much. Setting protection is covered in ",[19,1321,1323],{"href":1322},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Flock-cells-and-protect-a-sheet-with-openpyxl\u002F","Lock cells and protect a sheet with openpyxl",[121,1325,1327],{"id":1326},"choose-the-protection-that-matches-the-intent","Choose the protection that matches the intent",[10,1329,1330],{},"\"Protect the report\" means at least three different things, and picking the wrong one produces either a false sense of security or a file nobody can use:",[25,1332,34,1337,34,1340,34,1343,34,1346,34,1349,34,1353,34,1358,34,1363,34,1368,34,1372,34,1376,34,1379,34,1384,34,1387,34,1390,34,1393,34,1397,34,1400,34,1404,34,1407,34,1411,34,1414],{"viewBox":1333,"role":28,"ariaLabelledBy":1334,"xmlns":32,"style":33},"0 0 760 224",[1335,1336],"pw3-t","pw3-d",[36,1338,1339],{"id":1335},"Three levels of workbook protection and what each stops",[40,1341,1342],{"id":1336},"Sheet protection prevents accidental edits, encryption prevents reading without the password, and an authenticated download prevents the file being shared at all.",[44,1344],{"x":46,"y":46,"width":47,"height":1345,"fill":49},"224",[51,1347,1348],{"x":65,"y":829,"style":830},"What are you actually protecting against?",[44,1350],{"x":54,"y":73,"width":1351,"height":835,"rx":1352,"fill":115,"stroke":68},"226","12",[51,1354,1357],{"x":1355,"y":834,"style":1356},"141","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","sheet protection",[51,1359,1362],{"x":1355,"y":1360,"style":1361},"106","font-size:11.5px;fill:var(--text,#172033);text-anchor:middle","stops accidental edits",[51,1364,1367],{"x":1355,"y":1365,"style":1366},"130","font-size:11.5px;fill:var(--muted,#5b6780);text-anchor:middle","contents fully readable",[51,1369,1371],{"x":1355,"y":1370,"style":1366},"154","no help for confidentiality",[51,1373,1375],{"x":1355,"y":96,"style":1374},"font-size:11.5px;fill:var(--brand-strong,#4338ca);text-anchor:middle","for: shared templates",[44,1377],{"x":1378,"y":73,"width":1351,"height":835,"rx":1352,"fill":93,"stroke":68},"268",[51,1380,1383],{"x":1381,"y":834,"style":1382},"381","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","encryption",[51,1385,1386],{"x":1381,"y":1360,"style":1361},"unreadable without a key",[51,1388,1389],{"x":1381,"y":1365,"style":1366},"password must travel too",[51,1391,1392],{"x":1381,"y":1370,"style":1366},"lost password, lost file",[51,1394,1396],{"x":1381,"y":96,"style":1395},"font-size:11.5px;fill:var(--gold-ink,#7a4e06);text-anchor:middle","for: attachments in transit",[44,1398],{"x":1399,"y":73,"width":1345,"height":835,"rx":1352,"fill":102,"stroke":68},"508",[51,1401,1403],{"x":1402,"y":834,"style":60},"620","authenticated link",[51,1405,1406],{"x":1402,"y":1360,"style":1361},"access checked per person",[51,1408,1410],{"x":1402,"y":1365,"style":1409},"font-size:11.5px;fill:var(--teal-ink,#0b6157);text-anchor:middle","revocable, auditable",[51,1412,1413],{"x":1402,"y":1370,"style":1366},"needs somewhere to host it",[51,1415,1416],{"x":1402,"y":96,"style":1409},"for: recurring reports",[10,1418,1419,1420,23],{},"The third column is the one most teams should be moving towards. An encrypted attachment travels with its own password, usually in a second email, and cannot be revoked once sent; a link checked against an identity can be withdrawn the day someone changes team. The delivery mechanics are in ",[19,1421,1423],{"href":1422},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002F","Publishing Excel Reports to Cloud Storage",[121,1425,1427],{"id":1426},"fit-it-into-a-scheduled-ingest","Fit it into a scheduled ingest",[10,1429,1430],{},"The decryption step belongs at the very front of a pipeline, before anything else forms an opinion about the file. A small wrapper keeps the rest of the job unaware that encryption exists at all:",[126,1432,1434],{"className":169,"code":1433,"language":171,"meta":131,"style":131},"\"\"\"ingest.py — encryption handled once, at the boundary.\"\"\"\nimport io\nimport os\nfrom pathlib import Path\n\nimport msoffcrypto\nimport pandas as pd\n\nPASSWORD = os.environ.get(\"REPORT_PASSWORD\", \"\")\n\ndef open_workbook(path: Path) -> io.BytesIO:\n    \"\"\"Return readable workbook bytes, decrypting only if necessary.\"\"\"\n    raw = path.read_bytes()\n    if raw[:2] == b\"PK\":                 # already a plain zip\n        return io.BytesIO(raw)\n\n    buffer = io.BytesIO()\n    with io.BytesIO(raw) as source:\n        office_file = msoffcrypto.OfficeFile(source)\n        office_file.load_key(password=PASSWORD)\n        office_file.decrypt(buffer)\n    buffer.seek(0)\n    return buffer\n\nfor path in sorted(Path(\"incoming\").glob(\"*.xlsx\")):\n    df = pd.read_excel(open_workbook(path), engine=\"openpyxl\")\n    print(f\"{path.name}: {len(df):,} rows\")\n",[14,1435,1436,1441,1447,1453,1463,1467,1473,1483,1487,1505,1509,1519,1524,1534,1563,1571,1575,1584,1596,1605,1617,1622,1632,1640,1645,1669,1689],{"__ignoreMap":131},[135,1437,1438],{"class":63,"line":137},[135,1439,1440],{"class":144},"\"\"\"ingest.py — encryption handled once, at the boundary.\"\"\"\n",[135,1442,1443,1445],{"class":63,"line":181},[135,1444,185],{"class":184},[135,1446,189],{"class":188},[135,1448,1449,1451],{"class":63,"line":192},[135,1450,185],{"class":184},[135,1452,942],{"class":188},[135,1454,1455,1457,1459,1461],{"class":63,"line":199},[135,1456,427],{"class":184},[135,1458,1016],{"class":188},[135,1460,185],{"class":184},[135,1462,1021],{"class":188},[135,1464,1465],{"class":63,"line":207},[135,1466,196],{"emptyLinePlaceholder":195},[135,1468,1469,1471],{"class":63,"line":221},[135,1470,185],{"class":184},[135,1472,204],{"class":188},[135,1474,1475,1477,1479,1481],{"class":63,"line":226},[135,1476,185],{"class":184},[135,1478,212],{"class":188},[135,1480,215],{"class":184},[135,1482,218],{"class":188},[135,1484,1485],{"class":63,"line":257},[135,1486,196],{"emptyLinePlaceholder":195},[135,1488,1489,1492,1494,1496,1498,1500,1503],{"class":63,"line":269},[135,1490,1491],{"class":239},"PASSWORD",[135,1493,1043],{"class":184},[135,1495,956],{"class":188},[135,1497,959],{"class":144},[135,1499,248],{"class":188},[135,1501,1502],{"class":144},"\"\"",[135,1504,337],{"class":188},[135,1506,1507],{"class":63,"line":292},[135,1508,196],{"emptyLinePlaceholder":195},[135,1510,1511,1513,1516],{"class":63,"line":303},[135,1512,229],{"class":184},[135,1514,1515],{"class":232}," open_workbook",[135,1517,1518],{"class":188},"(path: Path) -> io.BytesIO:\n",[135,1520,1521],{"class":63,"line":318},[135,1522,1523],{"class":144},"    \"\"\"Return readable workbook bytes, decrypting only if necessary.\"\"\"\n",[135,1525,1526,1529,1531],{"class":63,"line":324},[135,1527,1528],{"class":188},"    raw ",[135,1530,263],{"class":184},[135,1532,1533],{"class":188}," path.read_bytes()\n",[135,1535,1536,1539,1542,1545,1548,1551,1554,1557,1560],{"class":63,"line":329},[135,1537,1538],{"class":184},"    if",[135,1540,1541],{"class":188}," raw[:",[135,1543,1544],{"class":239},"2",[135,1546,1547],{"class":188},"] ",[135,1549,1550],{"class":184},"==",[135,1552,1553],{"class":184}," b",[135,1555,1556],{"class":144},"\"PK\"",[135,1558,1559],{"class":188},":                 ",[135,1561,1562],{"class":552},"# already a plain zip\n",[135,1564,1565,1568],{"class":63,"line":340},[135,1566,1567],{"class":184},"        return",[135,1569,1570],{"class":188}," io.BytesIO(raw)\n",[135,1572,1573],{"class":63,"line":356},[135,1574,196],{"emptyLinePlaceholder":195},[135,1576,1577,1580,1582],{"class":63,"line":370},[135,1578,1579],{"class":188},"    buffer ",[135,1581,263],{"class":184},[135,1583,266],{"class":188},[135,1585,1586,1588,1591,1593],{"class":63,"line":375},[135,1587,272],{"class":184},[135,1589,1590],{"class":188}," io.BytesIO(raw) ",[135,1592,215],{"class":184},[135,1594,1595],{"class":188}," source:\n",[135,1597,1598,1600,1602],{"class":63,"line":400},[135,1599,295],{"class":188},[135,1601,263],{"class":184},[135,1603,1604],{"class":188}," msoffcrypto.OfficeFile(source)\n",[135,1606,1607,1609,1611,1613,1615],{"class":63,"line":803},[135,1608,306],{"class":188},[135,1610,310],{"class":309},[135,1612,263],{"class":184},[135,1614,1491],{"class":239},[135,1616,337],{"class":188},[135,1618,1619],{"class":63,"line":1222},[135,1620,1621],{"class":188},"        office_file.decrypt(buffer)\n",[135,1623,1625,1628,1630],{"class":63,"line":1624},22,[135,1626,1627],{"class":188},"    buffer.seek(",[135,1629,46],{"class":239},[135,1631,337],{"class":188},[135,1633,1635,1637],{"class":63,"line":1634},23,[135,1636,359],{"class":184},[135,1638,1639],{"class":188}," buffer\n",[135,1641,1643],{"class":63,"line":1642},24,[135,1644,196],{"emptyLinePlaceholder":195},[135,1646,1648,1650,1653,1655,1657,1660,1662,1665,1667],{"class":63,"line":1647},25,[135,1649,1105],{"class":184},[135,1651,1652],{"class":188}," path ",[135,1654,1111],{"class":184},[135,1656,1114],{"class":239},[135,1658,1659],{"class":188},"(Path(",[135,1661,1049],{"class":144},[135,1663,1664],{"class":188},").glob(",[135,1666,1124],{"class":144},[135,1668,1127],{"class":188},[135,1670,1672,1675,1677,1680,1683,1685,1687],{"class":63,"line":1671},26,[135,1673,1674],{"class":188},"    df ",[135,1676,263],{"class":184},[135,1678,1679],{"class":188}," pd.read_excel(open_workbook(path), ",[135,1681,1682],{"class":309},"engine",[135,1684,263],{"class":184},[135,1686,351],{"class":144},[135,1688,337],{"class":188},[135,1690,1692,1694,1696,1698,1700,1702,1705,1707,1710,1712,1715,1718,1721,1723,1726],{"class":63,"line":1691},27,[135,1693,1225],{"class":239},[135,1695,629],{"class":188},[135,1697,768],{"class":184},[135,1699,771],{"class":144},[135,1701,775],{"class":774},[135,1703,1704],{"class":188},"path.name",[135,1706,781],{"class":774},[135,1708,1709],{"class":144},": ",[135,1711,775],{"class":774},[135,1713,1714],{"class":239},"len",[135,1716,1717],{"class":188},"(df)",[135,1719,1720],{"class":184},":,",[135,1722,781],{"class":774},[135,1724,1725],{"class":144}," rows\"",[135,1727,337],{"class":188},[10,1729,1730,1731,1734],{},"Checking the first two bytes rather than calling ",[14,1732,1733],{},"is_encrypted()"," avoids opening the file twice, and it also handles the case where a sender switches protection on or off between months without telling anybody. Log which files needed decrypting — that record is useful when a password rotates and half the batch suddenly fails.",[121,1736,1738],{"id":1737},"common-pitfalls-and-gotchas","Common pitfalls and gotchas",[1740,1741,1742,1753,1759,1765,1771],"ul",{},[1743,1744,1745,1752],"li",{},[1746,1747,1748,1749,23],"strong",{},"Reading before ",[14,1750,1751],{},"seek(0)"," The decrypted buffer is positioned at the end after writing.",[1743,1754,1755,1758],{},[1746,1756,1757],{},"Assuming the error means corruption."," \"Not a zip file\" is exactly what an encrypted workbook looks like to openpyxl.",[1743,1760,1761,1764],{},[1746,1762,1763],{},"Leaving decrypted copies behind."," Delete them, or decrypt into memory and avoid the question.",[1743,1766,1767,1770],{},[1746,1768,1769],{},"Hard-coded passwords."," They end up in version control and in log lines built with f-strings.",[1743,1772,1773,1780],{},[1746,1774,1775,1776,1779],{},"Legacy ",[14,1777,1778],{},".xls"," encryption."," Older schemes are supported but weaker; if you control the sender, ask for a modern format.",[121,1782,1784],{"id":1783},"performance-and-scale-notes","Performance and scale notes",[10,1786,1787,1788,1791,1792,23],{},"Decryption is fast — it is a symmetric cipher over a file that is already compressed — so the cost is dominated by the subsequent Excel parse, exactly as it would be for a plain file. Memory is the consideration: the decrypted workbook exists in the buffer alongside the encrypted source, so peak usage is roughly twice the file size before parsing begins. For a large encrypted workbook, decrypt to a temporary file rather than a buffer, parse from the path, and delete it in a ",[14,1789,1790],{},"finally"," block. The same reasoning applies to any in-memory pipeline, as covered in ",[19,1793,1795],{"href":1794},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fbuild-an-excel-workbook-in-memory-with-bytesio\u002F","Build an Excel workbook in memory with BytesIO",[121,1797,1799],{"id":1798},"conclusion","Conclusion",[10,1801,1802,1803,1805,1806,1809],{},"An encrypted workbook is not a corrupt one — it is an OLE2 container that no zip-based reader can open. Decrypt it with ",[14,1804,16],{}," into a ",[14,1807,1808],{},"BytesIO",", then read it exactly as you would any other file. Detect encryption up front so an ingest loop can branch cleanly, raise a clear error on a wrong password, keep the password in the environment, and remember that sheet protection is a different thing entirely.",[121,1811,1813],{"id":1812},"frequently-asked-questions","Frequently asked questions",[10,1815,1816,1819],{},[1746,1817,1818],{},"Why does openpyxl say a protected file is not a zip?","\nAn encrypted workbook is an OLE2 container wrapping the encrypted payload, not a zip. openpyxl opens the zip first, so it fails before it can ask for a password. Decrypt the file first, then read the result.",[10,1821,1822,1825],{},[1746,1823,1824],{},"Is this the same as a sheet-protection password?","\nNo. Sheet and workbook-structure protection stop editing but leave the file readable — openpyxl opens it without any password. Encryption (\"Password to open\") makes the bytes unreadable until decrypted.",[10,1827,1828,1831],{},[1746,1829,1830],{},"Can Python remove the password permanently?","\nYou can write out a decrypted copy, which is what most pipelines want. Keep the encrypted original if the protection is a requirement rather than an obstacle.",[10,1833,1834,1837],{},[1746,1835,1836],{},"What if I do not have the password?","\nThen you cannot read the file. Modern Office encryption is AES-based and there is no supported way around it — ask the sender to re-issue the workbook or share the password through a secrets manager.",[10,1839,1840,1843,1844,1847],{},[1746,1841,1842],{},"Does the decrypted copy have to touch disk?","\nNo. msoffcrypto-tool writes to any file-like object, so decrypting into an ",[14,1845,1846],{},"io.BytesIO"," and reading from that keeps the plaintext workbook in memory only.",[121,1849,1851],{"id":1850},"related","Related",[1740,1853,1854,1863,1868,1873,1878],{},[1743,1855,1856,1859,1860,1862],{},[1746,1857,1858],{},"Up:"," ",[19,1861,22],{"href":21}," — the other side of the same subject.",[1743,1864,1865,1867],{},[19,1866,1246],{"href":1245}," — producing a protected file rather than reading one.",[1743,1869,1870,1872],{},[19,1871,1323],{"href":1322}," — the edit-prevention flags this page is often confused with.",[1743,1874,1875,1877],{},[19,1876,586],{"href":585}," — the error an encrypted file produces, among others.",[1743,1879,1880,1882],{},[19,1881,1795],{"href":1794}," — the buffer pattern the decryption relies on.",[1884,1885,1886],"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 .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":131,"searchDepth":181,"depth":181,"links":1888},[1889,1890,1891,1892,1893,1894,1895,1896,1897,1898,1899,1900,1901,1902,1903],{"id":123,"depth":181,"text":124},{"id":162,"depth":181,"text":163},{"id":468,"depth":181,"text":469},{"id":589,"depth":181,"text":590},{"id":926,"depth":181,"text":927},{"id":992,"depth":181,"text":993},{"id":1238,"depth":181,"text":1239},{"id":1250,"depth":181,"text":1251},{"id":1326,"depth":181,"text":1327},{"id":1426,"depth":181,"text":1427},{"id":1737,"depth":181,"text":1738},{"id":1783,"depth":181,"text":1784},{"id":1798,"depth":181,"text":1799},{"id":1812,"depth":181,"text":1813},{"id":1850,"depth":181,"text":1851},"2026-08-27","Decrypt an encrypted workbook in memory with msoffcrypto-tool, read it with pandas or openpyxl, handle wrong passwords, and tell encryption from sheet protection.","md",[1908,1910,1912,1914,1916],{"q":1818,"a":1909},"An encrypted workbook is an OLE2 container wrapping the encrypted payload, not a zip. openpyxl opens the zip first, so it fails before it can ask for a password. Decrypt the file first, then read the result.",{"q":1824,"a":1911},"No. Sheet and workbook-structure protection stop editing but leave the file readable — openpyxl opens it without any password. Encryption (\"Password to open\") makes the bytes unreadable until decrypted.",{"q":1830,"a":1913},"You can write out a decrypted copy, which is what most pipelines want. Keep the encrypted original if the protection is a requirement rather than an obstacle.",{"q":1836,"a":1915},"Then you cannot read the file. Modern Office encryption is AES-based and there is no supported way around it — ask the sender to re-issue the workbook or share the password through a secrets manager.",{"q":1842,"a":1917},"No. msoffcrypto-tool writes to any file-like object, so decrypting into an io.BytesIO and reading from that keeps the plaintext workbook in memory only.",{},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fopen-a-password-protected-excel-file-with-python",{"title":1921,"description":1922},"Read Password-Protected Excel in Python","Open encrypted xlsx files in Python: msoffcrypto-tool to decrypt into BytesIO, wrong-password handling, batch decryption, and the difference from sheet protection.","open-a-password-protected-excel-file-with-python","formatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fopen-a-password-protected-excel-file-with-python\u002Findex","how-to","AOk41bVG-2cy8i3k7DrJ_jnKdQSILXtcLcBlaGCoMOI",[1928,1932],{"title":1929,"path":1930,"stem":1931,"children":-1},"Lock Cells and Protect a Sheet with openpyxl","\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Flock-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",{"title":1933,"path":1934,"stem":1935,"children":-1},"Password Protect an Excel File with Python","\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",1788710159225]